Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions crates/uv-distribution/src/metadata/lowering.rs
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,9 @@ impl LoweredRequirement {
},
url,
}
} else if member.pyproject_toml().is_package() {
// We don't require a build system, because the workspace member is a
// dependency
} else if member.pyproject_toml().is_package(false) {
RequirementSource::Directory {
install_path: install_path.into_boxed_path(),
url,
Expand Down Expand Up @@ -736,7 +738,8 @@ fn path_source(
fs_err::read_to_string(&pyproject_path)
.ok()
.and_then(|contents| PyProjectToml::from_string(contents).ok())
.and_then(|pyproject_toml| pyproject_toml.tool_uv_package())
// We don't require a build system for path dependencies
.map(|pyproject_toml| pyproject_toml.is_package(false))
.unwrap_or(true)
});

Expand Down
6 changes: 5 additions & 1 deletion crates/uv-resolver/src/lock/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1255,6 +1255,7 @@ impl Lock {
root: &Path,
packages: &BTreeMap<PackageName, WorkspaceMember>,
members: &[PackageName],
required_members: BTreeSet<&PackageName>,
requirements: &[Requirement],
constraints: &[Requirement],
overrides: &[Requirement],
Expand Down Expand Up @@ -1282,7 +1283,10 @@ impl Lock {
// Validate that the member sources have not changed (e.g., that they've switched from
// virtual to non-virtual or vice versa).
for (name, member) in packages {
let expected = !member.pyproject_toml().is_package();
// We don't require a build system, if the workspace member is a dependency
let expected = !member
.pyproject_toml()
.is_package(!required_members.contains(name));
let actual = self
.find_by_name(name)
.ok()
Expand Down
8 changes: 4 additions & 4 deletions crates/uv-workspace/src/pyproject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ pub struct PyProjectToml {

/// Used to determine whether a `build-system` section is present.
#[serde(default, skip_serializing)]
build_system: Option<serde::de::IgnoredAny>,
pub build_system: Option<serde::de::IgnoredAny>,
}

impl PyProjectToml {
Expand All @@ -81,18 +81,18 @@ impl PyProjectToml {

/// Returns `true` if the project should be considered a Python package, as opposed to a
/// non-package ("virtual") project.
pub fn is_package(&self) -> bool {
pub fn is_package(&self, require_build_system: bool) -> bool {
// If `tool.uv.package` is set, defer to that explicit setting.
if let Some(is_package) = self.tool_uv_package() {
return is_package;
}

// Otherwise, a project is assumed to be a package if `build-system` is present.
self.build_system.is_some()
self.build_system.is_some() || !require_build_system
}

/// Returns the value of `tool.uv.package` if set.
pub fn tool_uv_package(&self) -> Option<bool> {
fn tool_uv_package(&self) -> Option<bool> {
self.tool
.as_ref()
.and_then(|tool| tool.uv.as_ref())
Expand Down
48 changes: 43 additions & 5 deletions crates/uv-workspace/src/workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use uv_warnings::warn_user_once;

use crate::dependency_groups::{DependencyGroupError, FlatDependencyGroup, FlatDependencyGroups};
use crate::pyproject::{
Project, PyProjectToml, PyprojectTomlError, Sources, ToolUvSources, ToolUvWorkspace,
Project, PyProjectToml, PyprojectTomlError, Source, Sources, ToolUvSources, ToolUvWorkspace,
};

type WorkspaceMembers = Arc<BTreeMap<PackageName, WorkspaceMember>>;
Expand Down Expand Up @@ -260,6 +260,7 @@ impl Workspace {
pyproject_toml: PyProjectToml,
) -> Option<Self> {
let mut packages = self.packages;

let member = Arc::make_mut(&mut packages).get_mut(package_name)?;

if member.root == self.install_path {
Expand Down Expand Up @@ -303,7 +304,7 @@ impl Workspace {

/// Returns the set of all workspace members.
pub fn members_requirements(&self) -> impl Iterator<Item = Requirement> + '_ {
self.packages.values().filter_map(|member| {
self.packages.iter().filter_map(|(name, member)| {
let url = VerbatimUrl::from_absolute_path(&member.root)
.expect("path is valid URL")
.with_given(member.root.to_string_lossy());
Expand All @@ -312,7 +313,10 @@ impl Workspace {
extras: Box::new([]),
groups: Box::new([]),
marker: MarkerTree::TRUE,
source: if member.pyproject_toml.is_package() {
source: if member
.pyproject_toml()
.is_package(!self.is_required_member(name))
{
RequirementSource::Directory {
install_path: member.root.clone().into_boxed_path(),
editable: Some(true),
Expand All @@ -332,9 +336,40 @@ impl Workspace {
})
}

/// Whether a given workspace member is required by another member.
pub fn required_members(&self) -> impl Iterator<Item = &PackageName> + '_ {
self.sources
.iter()
.chain(
self.packages
.values()
.filter_map(|member| {
member
.pyproject_toml
.tool
.as_ref()
.and_then(|tool| tool.uv.as_ref())
.and_then(|uv| uv.sources.as_ref())
.map(ToolUvSources::inner)
})
.flatten(),
)
.filter_map(|(package, sources)| {
sources
.iter()
.any(|source| matches!(source, Source::Workspace { .. }))
.then_some(package)
})
}

/// Whether a given workspace member is required by another member.
pub fn is_required_member(&self, name: &PackageName) -> bool {
self.required_members().any(|package| package == name)
}

/// Returns the set of all workspace member dependency groups.
pub fn group_requirements(&self) -> impl Iterator<Item = Requirement> + '_ {
self.packages.values().filter_map(|member| {
self.packages.iter().filter_map(|(name, member)| {
let url = VerbatimUrl::from_absolute_path(&member.root)
.expect("path is valid URL")
.with_given(member.root.to_string_lossy());
Expand Down Expand Up @@ -368,7 +403,10 @@ impl Workspace {
extras: Box::new([]),
groups: groups.into_boxed_slice(),
marker: MarkerTree::TRUE,
source: if member.pyproject_toml.is_package() {
source: if member
.pyproject_toml()
.is_package(!self.is_required_member(name))
{
RequirementSource::Directory {
install_path: member.root.clone().into_boxed_path(),
editable: Some(true),
Expand Down
4 changes: 2 additions & 2 deletions crates/uv/src/commands/build_frontend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ async fn build_impl(
.get(package)
.ok_or_else(|| anyhow::anyhow!("Package `{package}` not found in workspace"))?;

if !package.pyproject_toml().is_package() {
if !package.pyproject_toml().is_package(true) {
let name = &package.project().name;
let pyproject_toml = package.root().join("pyproject.toml");
return Err(anyhow::anyhow!(
Expand Down Expand Up @@ -300,7 +300,7 @@ async fn build_impl(
let packages: Vec<_> = workspace
.packages()
.values()
.filter(|package| package.pyproject_toml().is_package())
.filter(|package| package.pyproject_toml().is_package(true))
.map(|package| AnnotatedSource {
source: Source::Directory(Cow::Borrowed(package.root())),
package: Some(package.project().name.clone()),
Expand Down
4 changes: 4 additions & 0 deletions crates/uv/src/commands/project/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,7 @@ async fn do_lock(
// Collect the requirements, etc.
let members = target.members();
let packages = target.packages();
let required_members = target.required_members();
let requirements = target.requirements();
let overrides = target.overrides();
let constraints = target.constraints();
Expand Down Expand Up @@ -693,6 +694,7 @@ async fn do_lock(
target.install_path(),
packages,
&members,
required_members,
&requirements,
&dependency_groups,
&constraints,
Expand Down Expand Up @@ -906,6 +908,7 @@ impl ValidatedLock {
install_path: &Path,
packages: &BTreeMap<PackageName, WorkspaceMember>,
members: &[PackageName],
required_members: BTreeSet<&PackageName>,
requirements: &[Requirement],
dependency_groups: &BTreeMap<GroupName, Vec<Requirement>>,
constraints: &[Requirement],
Expand Down Expand Up @@ -1117,6 +1120,7 @@ impl ValidatedLock {
install_path,
packages,
members,
required_members,
requirements,
constraints,
overrides,
Expand Down
14 changes: 13 additions & 1 deletion crates/uv/src/commands/project/lock_target.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::collections::BTreeMap;
use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};

use itertools::Either;
Expand Down Expand Up @@ -154,6 +154,18 @@ impl<'lock> LockTarget<'lock> {
}
}

/// Return the set of required workspace members, i.e., those that are required by other
/// members.
pub(crate) fn required_members(self) -> BTreeSet<&'lock PackageName> {
match self {
Self::Workspace(workspace) => workspace.required_members().collect(),
Self::Script(_) => {
static EMPTY: BTreeSet<&PackageName> = BTreeSet::new();
EMPTY.clone()
}
}
}

/// Returns the set of supported environments for the [`LockTarget`].
pub(crate) fn environments(self) -> Option<&'lock SupportedEnvironments> {
match self {
Expand Down
2 changes: 1 addition & 1 deletion crates/uv/src/commands/project/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ pub(crate) async fn sync(
// TODO(lucab): improve warning content
// <https://github.com/astral-sh/uv/issues/7428>
if project.workspace().pyproject_toml().has_scripts()
&& !project.workspace().pyproject_toml().is_package()
&& !project.workspace().pyproject_toml().is_package(true)
{
warn_user!(
"Skipping installation of entry points (`project.scripts`) because this project is not packaged; to install entry points, set `tool.uv.package = true` or define a `build-system`"
Expand Down
22 changes: 16 additions & 6 deletions crates/uv/tests/it/edit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10351,8 +10351,9 @@ fn add_self() -> Result<()> {

----- stderr -----
Resolved 2 packages in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
Prepared 2 packages in [TIME]
Installed 2 packages in [TIME]
+ anyio==0.1.0 (from file://[TEMP_DIR]/)
+ typing-extensions==4.10.0
");

Expand Down Expand Up @@ -10389,7 +10390,10 @@ fn add_self() -> Result<()> {

----- stderr -----
Resolved 2 packages in [TIME]
Audited 1 package in [TIME]
Prepared 1 package in [TIME]
Uninstalled 1 package in [TIME]
Installed 1 package in [TIME]
~ anyio==0.1.0 (from file://[TEMP_DIR]/)
");

let pyproject_toml = context.read("pyproject.toml");
Expand Down Expand Up @@ -13173,7 +13177,9 @@ fn add_path_with_existing_workspace() -> Result<()> {
----- stderr -----
Added `dep` to workspace members
Resolved 3 packages in [TIME]
Audited in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ dep==0.1.0 (from file://[TEMP_DIR]/dep)
");

let pyproject_toml = context.read("pyproject.toml");
Expand Down Expand Up @@ -13250,7 +13256,9 @@ fn add_path_with_workspace() -> Result<()> {
----- stderr -----
Added `dep` to workspace members
Resolved 2 packages in [TIME]
Audited in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ dep==0.1.0 (from file://[TEMP_DIR]/dep)
");

let pyproject_toml = context.read("pyproject.toml");
Expand Down Expand Up @@ -13316,7 +13324,9 @@ fn add_path_within_workspace_defaults_to_workspace() -> Result<()> {
----- stderr -----
Added `dep` to workspace members
Resolved 2 packages in [TIME]
Audited in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ dep==0.1.0 (from file://[TEMP_DIR]/dep)
");

let pyproject_toml = context.read("pyproject.toml");
Expand Down
Loading
Loading