mirror of
https://github.com/astral-sh/uv.git
synced 2025-11-20 03:49:54 +00:00
Build and install workspace members that are dependencies by default (#14663)
Regardless of the presence of a build system, as in https://github.com/astral-sh/uv/pull/14413 --------- Co-authored-by: John Mumm <jtfmumm@gmail.com>
This commit is contained in:
parent
0077f2357f
commit
cd40a34522
15 changed files with 791 additions and 67 deletions
|
|
@ -306,7 +306,10 @@ impl LoweredRequirement {
|
|||
},
|
||||
url,
|
||||
}
|
||||
} else if member.pyproject_toml().is_package() {
|
||||
} else if member
|
||||
.pyproject_toml()
|
||||
.is_package(!workspace.is_required_member(&requirement.name))
|
||||
{
|
||||
RequirementSource::Directory {
|
||||
install_path: install_path.into_boxed_path(),
|
||||
url,
|
||||
|
|
@ -736,7 +739,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)
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -771,7 +771,7 @@ mod tests {
|
|||
/// A reference list can be generated with:
|
||||
/// ```text
|
||||
/// $ python -c "from packaging import tags; [print(tag) for tag in tags.platform_tags()]"`
|
||||
/// ````
|
||||
/// ```
|
||||
#[test]
|
||||
fn test_platform_tags_manylinux() {
|
||||
let tags = compatible_tags(&Platform::new(
|
||||
|
|
|
|||
|
|
@ -1255,6 +1255,7 @@ impl Lock {
|
|||
root: &Path,
|
||||
packages: &BTreeMap<PackageName, WorkspaceMember>,
|
||||
members: &[PackageName],
|
||||
required_members: &BTreeSet<PackageName>,
|
||||
requirements: &[Requirement],
|
||||
constraints: &[Requirement],
|
||||
overrides: &[Requirement],
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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>>;
|
||||
|
|
@ -109,6 +109,8 @@ pub struct Workspace {
|
|||
install_path: PathBuf,
|
||||
/// The members of the workspace.
|
||||
packages: WorkspaceMembers,
|
||||
/// The workspace members that are required by other members.
|
||||
required_members: BTreeSet<PackageName>,
|
||||
/// The sources table from the workspace `pyproject.toml`.
|
||||
///
|
||||
/// This table is overridden by the project sources.
|
||||
|
|
@ -260,6 +262,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 {
|
||||
|
|
@ -279,17 +282,33 @@ impl Workspace {
|
|||
// Set the `pyproject.toml` for the member.
|
||||
member.pyproject_toml = pyproject_toml;
|
||||
|
||||
// Recompute required_members with the updated data
|
||||
let required_members = Self::collect_required_members(
|
||||
&packages,
|
||||
&workspace_sources,
|
||||
&workspace_pyproject_toml,
|
||||
);
|
||||
|
||||
Some(Self {
|
||||
pyproject_toml: workspace_pyproject_toml,
|
||||
sources: workspace_sources,
|
||||
packages,
|
||||
required_members,
|
||||
..self
|
||||
})
|
||||
} else {
|
||||
// Set the `pyproject.toml` for the member.
|
||||
member.pyproject_toml = pyproject_toml;
|
||||
|
||||
Some(Self { packages, ..self })
|
||||
// Recompute required_members with the updated member data
|
||||
let required_members =
|
||||
Self::collect_required_members(&packages, &self.sources, &self.pyproject_toml);
|
||||
|
||||
Some(Self {
|
||||
packages,
|
||||
required_members,
|
||||
..self
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -303,7 +322,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());
|
||||
|
|
@ -312,7 +331,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),
|
||||
|
|
@ -332,9 +354,65 @@ impl Workspace {
|
|||
})
|
||||
}
|
||||
|
||||
/// The workspace members that are required my another member of the workspace.
|
||||
pub fn required_members(&self) -> &BTreeSet<PackageName> {
|
||||
&self.required_members
|
||||
}
|
||||
|
||||
/// Compute the workspace members that are required by another member of the workspace.
|
||||
///
|
||||
/// N.B. this checks if a workspace member is required by inspecting `tool.uv.source` entries,
|
||||
/// but does not actually check if the source is _used_, which could result in false positives
|
||||
/// but is easier to compute.
|
||||
fn collect_required_members(
|
||||
packages: &BTreeMap<PackageName, WorkspaceMember>,
|
||||
sources: &BTreeMap<PackageName, Sources>,
|
||||
pyproject_toml: &PyProjectToml,
|
||||
) -> BTreeSet<PackageName> {
|
||||
sources
|
||||
.iter()
|
||||
.filter(|(name, _)| {
|
||||
pyproject_toml
|
||||
.project
|
||||
.as_ref()
|
||||
.is_none_or(|project| project.name != **name)
|
||||
})
|
||||
.chain(
|
||||
packages
|
||||
.iter()
|
||||
.filter_map(|(name, member)| {
|
||||
member
|
||||
.pyproject_toml
|
||||
.tool
|
||||
.as_ref()
|
||||
.and_then(|tool| tool.uv.as_ref())
|
||||
.and_then(|uv| uv.sources.as_ref())
|
||||
.map(ToolUvSources::inner)
|
||||
.map(move |sources| {
|
||||
sources
|
||||
.iter()
|
||||
.filter(move |(source_name, _)| name != *source_name)
|
||||
})
|
||||
})
|
||||
.flatten(),
|
||||
)
|
||||
.filter_map(|(package, sources)| {
|
||||
sources
|
||||
.iter()
|
||||
.any(|source| matches!(source, Source::Workspace { .. }))
|
||||
.then_some(package.clone())
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Whether a given workspace member is required by another member.
|
||||
pub fn is_required_member(&self, name: &PackageName) -> bool {
|
||||
self.required_members().contains(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());
|
||||
|
|
@ -368,7 +446,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),
|
||||
|
|
@ -746,9 +827,16 @@ impl Workspace {
|
|||
.and_then(|uv| uv.index)
|
||||
.unwrap_or_default();
|
||||
|
||||
let required_members = Self::collect_required_members(
|
||||
&workspace_members,
|
||||
&workspace_sources,
|
||||
&workspace_pyproject_toml,
|
||||
);
|
||||
|
||||
Ok(Workspace {
|
||||
install_path: workspace_root,
|
||||
packages: workspace_members,
|
||||
required_members,
|
||||
sources: workspace_sources,
|
||||
indexes: workspace_indexes,
|
||||
pyproject_toml: workspace_pyproject_toml,
|
||||
|
|
@ -1232,15 +1320,23 @@ impl ProjectWorkspace {
|
|||
project.name.clone(),
|
||||
current_project,
|
||||
)]));
|
||||
let workspace_sources = BTreeMap::default();
|
||||
let required_members = Workspace::collect_required_members(
|
||||
¤t_project_as_members,
|
||||
&workspace_sources,
|
||||
project_pyproject_toml,
|
||||
);
|
||||
|
||||
return Ok(Self {
|
||||
project_root: project_path.clone(),
|
||||
project_name: project.name.clone(),
|
||||
workspace: Workspace {
|
||||
install_path: project_path.clone(),
|
||||
packages: current_project_as_members,
|
||||
required_members,
|
||||
// There may be package sources, but we don't need to duplicate them into the
|
||||
// workspace sources.
|
||||
sources: BTreeMap::default(),
|
||||
sources: workspace_sources,
|
||||
indexes: Vec::default(),
|
||||
pyproject_toml: project_pyproject_toml.clone(),
|
||||
},
|
||||
|
|
@ -1692,6 +1788,7 @@ mod tests {
|
|||
"pyproject_toml": "[PYPROJECT_TOML]"
|
||||
}
|
||||
},
|
||||
"required_members": [],
|
||||
"sources": {},
|
||||
"indexes": [],
|
||||
"pyproject_toml": {
|
||||
|
|
@ -1745,6 +1842,7 @@ mod tests {
|
|||
"pyproject_toml": "[PYPROJECT_TOML]"
|
||||
}
|
||||
},
|
||||
"required_members": [],
|
||||
"sources": {},
|
||||
"indexes": [],
|
||||
"pyproject_toml": {
|
||||
|
|
@ -1825,6 +1923,10 @@ mod tests {
|
|||
"pyproject_toml": "[PYPROJECT_TOML]"
|
||||
}
|
||||
},
|
||||
"required_members": [
|
||||
"bird-feeder",
|
||||
"seeds"
|
||||
],
|
||||
"sources": {
|
||||
"bird-feeder": [
|
||||
{
|
||||
|
|
@ -1946,6 +2048,10 @@ mod tests {
|
|||
"pyproject_toml": "[PYPROJECT_TOML]"
|
||||
}
|
||||
},
|
||||
"required_members": [
|
||||
"bird-feeder",
|
||||
"seeds"
|
||||
],
|
||||
"sources": {},
|
||||
"indexes": [],
|
||||
"pyproject_toml": {
|
||||
|
|
@ -2013,6 +2119,7 @@ mod tests {
|
|||
"pyproject_toml": "[PYPROJECT_TOML]"
|
||||
}
|
||||
},
|
||||
"required_members": [],
|
||||
"sources": {},
|
||||
"indexes": [],
|
||||
"pyproject_toml": {
|
||||
|
|
@ -2147,6 +2254,7 @@ mod tests {
|
|||
"pyproject_toml": "[PYPROJECT_TOML]"
|
||||
}
|
||||
},
|
||||
"required_members": [],
|
||||
"sources": {},
|
||||
"indexes": [],
|
||||
"pyproject_toml": {
|
||||
|
|
@ -2254,6 +2362,7 @@ mod tests {
|
|||
"pyproject_toml": "[PYPROJECT_TOML]"
|
||||
}
|
||||
},
|
||||
"required_members": [],
|
||||
"sources": {},
|
||||
"indexes": [],
|
||||
"pyproject_toml": {
|
||||
|
|
@ -2375,6 +2484,7 @@ mod tests {
|
|||
"pyproject_toml": "[PYPROJECT_TOML]"
|
||||
}
|
||||
},
|
||||
"required_members": [],
|
||||
"sources": {},
|
||||
"indexes": [],
|
||||
"pyproject_toml": {
|
||||
|
|
@ -2470,6 +2580,7 @@ mod tests {
|
|||
"pyproject_toml": "[PYPROJECT_TOML]"
|
||||
}
|
||||
},
|
||||
"required_members": [],
|
||||
"sources": {},
|
||||
"indexes": [],
|
||||
"pyproject_toml": {
|
||||
|
|
|
|||
|
|
@ -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!(
|
||||
|
|
@ -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()),
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
@ -693,6 +694,7 @@ async fn do_lock(
|
|||
target.install_path(),
|
||||
packages,
|
||||
&members,
|
||||
required_members,
|
||||
&requirements,
|
||||
&dependency_groups,
|
||||
&constraints,
|
||||
|
|
@ -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],
|
||||
|
|
@ -1117,6 +1120,7 @@ impl ValidatedLock {
|
|||
install_path,
|
||||
packages,
|
||||
members,
|
||||
required_members,
|
||||
requirements,
|
||||
constraints,
|
||||
overrides,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use std::collections::BTreeMap;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use itertools::Either;
|
||||
|
|
@ -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) -> &'lock BTreeSet<PackageName> {
|
||||
match self {
|
||||
Self::Workspace(workspace) => workspace.required_members(),
|
||||
Self::Script(_) => {
|
||||
static EMPTY: BTreeSet<PackageName> = BTreeSet::new();
|
||||
&EMPTY
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the set of supported environments for the [`LockTarget`].
|
||||
pub(crate) fn environments(self) -> Option<&'lock SupportedEnvironments> {
|
||||
match self {
|
||||
|
|
|
|||
|
|
@ -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`"
|
||||
|
|
|
|||
|
|
@ -10362,7 +10362,7 @@ fn add_self() -> Result<()> {
|
|||
filters => context.filters(),
|
||||
}, {
|
||||
assert_snapshot!(
|
||||
pyproject_toml, @r###"
|
||||
pyproject_toml, @r#"
|
||||
[project]
|
||||
name = "anyio"
|
||||
version = "0.1.0"
|
||||
|
|
@ -10377,7 +10377,7 @@ fn add_self() -> Result<()> {
|
|||
|
||||
[tool.uv.sources]
|
||||
anyio = { workspace = true }
|
||||
"###
|
||||
"#
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -10398,7 +10398,7 @@ fn add_self() -> Result<()> {
|
|||
filters => context.filters(),
|
||||
}, {
|
||||
assert_snapshot!(
|
||||
pyproject_toml, @r###"
|
||||
pyproject_toml, @r#"
|
||||
[project]
|
||||
name = "anyio"
|
||||
version = "0.1.0"
|
||||
|
|
@ -10418,7 +10418,7 @@ fn add_self() -> Result<()> {
|
|||
dev = [
|
||||
"anyio[types]",
|
||||
]
|
||||
"###
|
||||
"#
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -13173,7 +13173,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");
|
||||
|
|
@ -13250,7 +13252,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");
|
||||
|
|
@ -13316,7 +13320,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");
|
||||
|
|
|
|||
|
|
@ -12064,10 +12064,6 @@ fn lock_remove_member() -> Result<()> {
|
|||
requires-python = ">=3.12"
|
||||
dependencies = ["leaf"]
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools>=42"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[tool.uv.workspace]
|
||||
members = ["leaf"]
|
||||
|
||||
|
|
@ -12130,7 +12126,7 @@ fn lock_remove_member() -> Result<()> {
|
|||
[[package]]
|
||||
name = "leaf"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "leaf" }
|
||||
source = { editable = "leaf" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
]
|
||||
|
|
@ -12141,13 +12137,13 @@ fn lock_remove_member() -> Result<()> {
|
|||
[[package]]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
source = { editable = "." }
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "leaf" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [{ name = "leaf", virtual = "leaf" }]
|
||||
requires-dist = [{ name = "leaf", editable = "leaf" }]
|
||||
|
||||
[[package]]
|
||||
name = "sniffio"
|
||||
|
|
@ -12162,16 +12158,124 @@ fn lock_remove_member() -> Result<()> {
|
|||
});
|
||||
|
||||
// Re-run with `--locked`.
|
||||
uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r###"
|
||||
uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 5 packages in [TIME]
|
||||
"###);
|
||||
");
|
||||
|
||||
// Remove the member.
|
||||
// Remove the member as a dependency (retain it as a workspace member)
|
||||
pyproject_toml.write_str(
|
||||
r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = []
|
||||
|
||||
[tool.uv.workspace]
|
||||
members = ["leaf"]
|
||||
|
||||
[tool.uv.sources]
|
||||
leaf = { workspace = true }
|
||||
"#,
|
||||
)?;
|
||||
|
||||
// Re-run with `--locked`. This should fail.
|
||||
uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 5 packages in [TIME]
|
||||
The lockfile at `uv.lock` needs to be updated, but `--locked` was provided. To update the lockfile, run `uv lock`.
|
||||
");
|
||||
|
||||
// Re-run without `--locked`.
|
||||
uv_snapshot!(context.filters(), context.lock(), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 5 packages in [TIME]
|
||||
");
|
||||
|
||||
let lock = context.read("uv.lock");
|
||||
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
assert_snapshot!(
|
||||
lock, @r#"
|
||||
version = 1
|
||||
revision = 2
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[options]
|
||||
exclude-newer = "2024-03-25T00:00:00Z"
|
||||
|
||||
[manifest]
|
||||
members = [
|
||||
"leaf",
|
||||
"project",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "4.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "idna" },
|
||||
{ name = "sniffio" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/db/4d/3970183622f0330d3c23d9b8a5f52e365e50381fd484d08e3285104333d3/anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6", size = 159642, upload-time = "2024-02-19T08:36:28.641Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8", size = 85584, upload-time = "2024-02-19T08:36:26.842Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426, upload-time = "2023-11-25T15:40:54.902Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567, upload-time = "2023-11-25T15:40:52.604Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "leaf"
|
||||
version = "0.1.0"
|
||||
source = { editable = "leaf" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [{ name = "anyio", specifier = ">3" }]
|
||||
|
||||
[[package]]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
|
||||
[[package]]
|
||||
name = "sniffio"
|
||||
version = "1.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
|
||||
]
|
||||
"#
|
||||
);
|
||||
});
|
||||
|
||||
// Remove the member entirely
|
||||
pyproject_toml.write_str(
|
||||
r#"
|
||||
[project]
|
||||
|
|
@ -12238,7 +12342,7 @@ fn lock_remove_member() -> Result<()> {
|
|||
/// This test would fail if we didn't write the list of workspace members to the lockfile, since
|
||||
/// we wouldn't be able to determine that a new member was added.
|
||||
#[test]
|
||||
fn lock_add_member() -> Result<()> {
|
||||
fn lock_add_member_with_build_system() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
// Create a workspace, but don't add the member.
|
||||
|
|
@ -12449,6 +12553,339 @@ fn lock_add_member() -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lock_add_member_without_build_system() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
// Create a workspace, but don't add the member.
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(
|
||||
r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = []
|
||||
|
||||
[tool.uv.workspace]
|
||||
members = []
|
||||
"#,
|
||||
)?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.lock(), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 1 package in [TIME]
|
||||
"###);
|
||||
|
||||
let lock = context.read("uv.lock");
|
||||
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
assert_snapshot!(
|
||||
lock, @r#"
|
||||
version = 1
|
||||
revision = 2
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[options]
|
||||
exclude-newer = "2024-03-25T00:00:00Z"
|
||||
|
||||
[[package]]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
"#
|
||||
);
|
||||
});
|
||||
|
||||
// Re-run with `--locked`.
|
||||
uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 1 package in [TIME]
|
||||
"###);
|
||||
|
||||
// Create a workspace member.
|
||||
let leaf = context.temp_dir.child("leaf");
|
||||
leaf.child("pyproject.toml").write_str(
|
||||
r#"
|
||||
[project]
|
||||
name = "leaf"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = ["anyio>3"]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
// Add the member to the workspace, but not as a dependency of the root.
|
||||
pyproject_toml.write_str(
|
||||
r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = []
|
||||
|
||||
[tool.uv.workspace]
|
||||
members = ["leaf"]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
// Re-run with `--locked`. This should fail.
|
||||
uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 5 packages in [TIME]
|
||||
The lockfile at `uv.lock` needs to be updated, but `--locked` was provided. To update the lockfile, run `uv lock`.
|
||||
");
|
||||
|
||||
// Re-run with `--offline`. This should also fail, during the resolve phase.
|
||||
uv_snapshot!(context.filters(), context.lock().arg("--locked").arg("--offline").arg("--no-cache"), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
× No solution found when resolving dependencies:
|
||||
╰─▶ Because anyio was not found in the cache and leaf depends on anyio>3, we can conclude that leaf's requirements are unsatisfiable.
|
||||
And because your workspace requires leaf, we can conclude that your workspace's requirements are unsatisfiable.
|
||||
|
||||
hint: Packages were unavailable because the network was disabled. When the network is disabled, registry packages may only be read from the cache.
|
||||
"###);
|
||||
|
||||
// Re-run without `--locked`.
|
||||
uv_snapshot!(context.filters(), context.lock(), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 5 packages in [TIME]
|
||||
Added anyio v4.3.0
|
||||
Added idna v3.6
|
||||
Added leaf v0.1.0
|
||||
Added sniffio v1.3.1
|
||||
"###);
|
||||
|
||||
// Re-run with `--locked`.
|
||||
uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 5 packages in [TIME]
|
||||
"###);
|
||||
|
||||
let lock = context.read("uv.lock");
|
||||
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
assert_snapshot!(
|
||||
lock, @r#"
|
||||
version = 1
|
||||
revision = 2
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[options]
|
||||
exclude-newer = "2024-03-25T00:00:00Z"
|
||||
|
||||
[manifest]
|
||||
members = [
|
||||
"leaf",
|
||||
"project",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "4.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "idna" },
|
||||
{ name = "sniffio" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/db/4d/3970183622f0330d3c23d9b8a5f52e365e50381fd484d08e3285104333d3/anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6", size = 159642, upload-time = "2024-02-19T08:36:28.641Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8", size = 85584, upload-time = "2024-02-19T08:36:26.842Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426, upload-time = "2023-11-25T15:40:54.902Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567, upload-time = "2023-11-25T15:40:52.604Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "leaf"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "leaf" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [{ name = "anyio", specifier = ">3" }]
|
||||
|
||||
[[package]]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
|
||||
[[package]]
|
||||
name = "sniffio"
|
||||
version = "1.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
|
||||
]
|
||||
"#
|
||||
);
|
||||
});
|
||||
|
||||
// Add the member to the workspace, as a dependency of the root.
|
||||
pyproject_toml.write_str(
|
||||
r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = ["leaf"]
|
||||
|
||||
[tool.uv.workspace]
|
||||
members = ["leaf"]
|
||||
|
||||
[tool.uv.sources]
|
||||
leaf = { workspace = true }
|
||||
"#,
|
||||
)?;
|
||||
|
||||
// Re-run with `--locked`. This should fail.
|
||||
uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 5 packages in [TIME]
|
||||
The lockfile at `uv.lock` needs to be updated, but `--locked` was provided. To update the lockfile, run `uv lock`.
|
||||
");
|
||||
|
||||
// Re-run without `--locked`.
|
||||
uv_snapshot!(context.filters(), context.lock(), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 5 packages in [TIME]
|
||||
");
|
||||
|
||||
// Re-run with `--locked`.
|
||||
uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 5 packages in [TIME]
|
||||
");
|
||||
|
||||
let lock = context.read("uv.lock");
|
||||
|
||||
// It should change from a virtual to an editable source
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
assert_snapshot!(
|
||||
lock, @r#"
|
||||
version = 1
|
||||
revision = 2
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[options]
|
||||
exclude-newer = "2024-03-25T00:00:00Z"
|
||||
|
||||
[manifest]
|
||||
members = [
|
||||
"leaf",
|
||||
"project",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "4.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "idna" },
|
||||
{ name = "sniffio" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/db/4d/3970183622f0330d3c23d9b8a5f52e365e50381fd484d08e3285104333d3/anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6", size = 159642, upload-time = "2024-02-19T08:36:28.641Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8", size = 85584, upload-time = "2024-02-19T08:36:26.842Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426, upload-time = "2023-11-25T15:40:54.902Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567, upload-time = "2023-11-25T15:40:52.604Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "leaf"
|
||||
version = "0.1.0"
|
||||
source = { editable = "leaf" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [{ name = "anyio", specifier = ">3" }]
|
||||
|
||||
[[package]]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "leaf" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [{ name = "leaf", editable = "leaf" }]
|
||||
|
||||
[[package]]
|
||||
name = "sniffio"
|
||||
version = "1.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
|
||||
]
|
||||
"#
|
||||
);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Lock a `pyproject.toml`, then add a dependency that's already included in the resolution.
|
||||
/// In theory, we shouldn't need to re-resolve, but based on our current strategy, we don't accept
|
||||
/// the existing lockfile.
|
||||
|
|
|
|||
|
|
@ -1094,18 +1094,19 @@ fn extra_unconditional() -> Result<()> {
|
|||
"###);
|
||||
// This is fine because we are only enabling one
|
||||
// extra, and thus, there is no conflict.
|
||||
uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###"
|
||||
uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Prepared 3 packages in [TIME]
|
||||
Installed 3 packages in [TIME]
|
||||
Prepared 4 packages in [TIME]
|
||||
Installed 4 packages in [TIME]
|
||||
+ anyio==4.1.0
|
||||
+ idna==3.6
|
||||
+ proxy1==0.1.0 (from file://[TEMP_DIR]/proxy1)
|
||||
+ sniffio==1.3.1
|
||||
"###);
|
||||
");
|
||||
|
||||
// And same thing for the other extra.
|
||||
root_pyproject_toml.write_str(
|
||||
|
|
@ -1215,18 +1216,19 @@ fn extra_unconditional_non_conflicting() -> Result<()> {
|
|||
// `uv sync` wasn't correctly propagating extras in a way
|
||||
// that would satisfy the conflict markers that got added
|
||||
// to the `proxy1[extra1]` dependency.
|
||||
uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###"
|
||||
uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Prepared 3 packages in [TIME]
|
||||
Installed 3 packages in [TIME]
|
||||
Prepared 4 packages in [TIME]
|
||||
Installed 4 packages in [TIME]
|
||||
+ anyio==4.1.0
|
||||
+ idna==3.6
|
||||
+ proxy1==0.1.0 (from file://[TEMP_DIR]/proxy1)
|
||||
+ sniffio==1.3.1
|
||||
"###);
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1301,16 +1303,17 @@ fn extra_unconditional_in_optional() -> Result<()> {
|
|||
"###);
|
||||
|
||||
// This should install `sortedcontainers==2.3.0`.
|
||||
uv_snapshot!(context.filters(), context.sync().arg("--frozen").arg("--extra=x1"), @r###"
|
||||
uv_snapshot!(context.filters(), context.sync().arg("--frozen").arg("--extra=x1"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Prepared 1 package in [TIME]
|
||||
Installed 1 package in [TIME]
|
||||
Prepared 2 packages in [TIME]
|
||||
Installed 2 packages in [TIME]
|
||||
+ proxy1==0.1.0 (from file://[TEMP_DIR]/proxy1)
|
||||
+ sortedcontainers==2.3.0
|
||||
"###);
|
||||
");
|
||||
|
||||
// This should install `sortedcontainers==2.4.0`.
|
||||
uv_snapshot!(context.filters(), context.sync().arg("--frozen").arg("--extra=x2"), @r###"
|
||||
|
|
@ -4460,19 +4463,20 @@ conflicts = [
|
|||
error: Extra `x2` is not defined in the project's `optional-dependencies` table
|
||||
"###);
|
||||
|
||||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||||
uv_snapshot!(context.filters(), context.sync(), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 7 packages in [TIME]
|
||||
Prepared 3 packages in [TIME]
|
||||
Installed 3 packages in [TIME]
|
||||
Prepared 4 packages in [TIME]
|
||||
Installed 4 packages in [TIME]
|
||||
+ anyio==4.3.0
|
||||
+ idna==3.6
|
||||
+ proxy1==0.1.0 (from file://[TEMP_DIR]/proxy1)
|
||||
+ sniffio==1.3.1
|
||||
"###);
|
||||
");
|
||||
|
||||
let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap();
|
||||
insta::with_settings!({
|
||||
|
|
@ -4558,14 +4562,14 @@ conflicts = [
|
|||
requires-dist = [
|
||||
{ name = "anyio", specifier = ">=4" },
|
||||
{ name = "idna", marker = "extra == 'x1'", specifier = "==3.6" },
|
||||
{ name = "proxy1", virtual = "proxy1" },
|
||||
{ name = "proxy1", editable = "proxy1" },
|
||||
]
|
||||
provides-extras = ["x1"]
|
||||
|
||||
[[package]]
|
||||
name = "proxy1"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "proxy1" }
|
||||
source = { editable = "proxy1" }
|
||||
|
||||
[package.optional-dependencies]
|
||||
x2 = [
|
||||
|
|
|
|||
|
|
@ -15772,18 +15772,18 @@ fn project_and_group_workspace_inherit() -> Result<()> {
|
|||
----- stdout -----
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile --cache-dir [CACHE_DIR] --group packages/mysubproject/pyproject.toml:foo
|
||||
-e file://[TEMP_DIR]/packages/pytest
|
||||
# via mysubproject (packages/mysubproject/pyproject.toml:foo)
|
||||
-e file://[TEMP_DIR]/packages/sniffio
|
||||
# via
|
||||
# mysubproject (packages/mysubproject/pyproject.toml:foo)
|
||||
# anyio
|
||||
anyio==4.3.0
|
||||
# via mysubproject (packages/mysubproject/pyproject.toml:foo)
|
||||
idna==3.6
|
||||
# via anyio
|
||||
iniconfig==2.0.0
|
||||
# via mysubproject (packages/mysubproject/pyproject.toml:foo)
|
||||
pytest @ file://[TEMP_DIR]/packages/pytest
|
||||
# via mysubproject (packages/mysubproject/pyproject.toml:foo)
|
||||
sniffio @ file://[TEMP_DIR]/packages/sniffio
|
||||
# via
|
||||
# mysubproject (packages/mysubproject/pyproject.toml:foo)
|
||||
# anyio
|
||||
|
||||
----- stderr -----
|
||||
Resolved 5 packages in [TIME]
|
||||
|
|
|
|||
|
|
@ -3565,6 +3565,101 @@ fn sync_ignore_extras_check_when_no_provides_extras() -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_workspace_members_with_transitive_dependencies() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(
|
||||
r#"
|
||||
[tool.uv.workspace]
|
||||
members = [
|
||||
"packages/*",
|
||||
]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let packages = context.temp_dir.child("packages");
|
||||
packages.create_dir_all()?;
|
||||
|
||||
// Create three workspace members with transitive dependency from
|
||||
// pkg-c -> pkg-b -> pkg-a
|
||||
let pkg_a = packages.child("pkg-a");
|
||||
pkg_a.create_dir_all()?;
|
||||
let pkg_a_pyproject_toml = pkg_a.child("pyproject.toml");
|
||||
pkg_a_pyproject_toml.write_str(
|
||||
r#"
|
||||
[project]
|
||||
name = "pkg-a"
|
||||
version = "0.0.1"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = ["anyio"]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let pkg_b = packages.child("pkg-b");
|
||||
pkg_b.create_dir_all()?;
|
||||
let pkg_b_pyproject_toml = pkg_b.child("pyproject.toml");
|
||||
pkg_b_pyproject_toml.write_str(
|
||||
r#"
|
||||
[project]
|
||||
name = "pkg-b"
|
||||
version = "0.0.1"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = ["pkg-a"]
|
||||
|
||||
[tool.uv.sources]
|
||||
pkg-a = { workspace = true }
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let pkg_c = packages.child("pkg-c");
|
||||
pkg_c.create_dir_all()?;
|
||||
let pkg_c_pyproject_toml = pkg_c.child("pyproject.toml");
|
||||
pkg_c_pyproject_toml.write_str(
|
||||
r#"
|
||||
[project]
|
||||
name = "pkg-c"
|
||||
version = "0.0.1"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = ["pkg-b"]
|
||||
|
||||
[tool.uv.sources]
|
||||
pkg-b = { workspace = true }
|
||||
"#,
|
||||
)?;
|
||||
|
||||
// Syncing should build the two transitive dependencies pkg-a and pkg-b,
|
||||
// but not pkg-c, which is not a dependency.
|
||||
uv_snapshot!(context.filters(), context.sync(), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 6 packages in [TIME]
|
||||
Prepared 5 packages in [TIME]
|
||||
Installed 5 packages in [TIME]
|
||||
+ anyio==4.3.0
|
||||
+ idna==3.6
|
||||
+ pkg-a==0.0.1 (from file://[TEMP_DIR]/packages/pkg-a)
|
||||
+ pkg-b==0.0.1 (from file://[TEMP_DIR]/packages/pkg-b)
|
||||
+ sniffio==1.3.1
|
||||
");
|
||||
|
||||
// The lockfile should be valid.
|
||||
uv_snapshot!(context.filters(), context.lock().arg("--check"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 6 packages in [TIME]
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_non_existent_extra_workspace_member() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
|
@ -3626,9 +3721,10 @@ fn sync_non_existent_extra_workspace_member() -> Result<()> {
|
|||
|
||||
----- stderr -----
|
||||
Resolved 5 packages in [TIME]
|
||||
Prepared 3 packages in [TIME]
|
||||
Installed 3 packages in [TIME]
|
||||
Prepared 4 packages in [TIME]
|
||||
Installed 4 packages in [TIME]
|
||||
+ anyio==4.3.0
|
||||
+ child==0.1.0 (from file://[TEMP_DIR]/child)
|
||||
+ idna==3.6
|
||||
+ sniffio==1.3.1
|
||||
");
|
||||
|
|
|
|||
|
|
@ -808,9 +808,9 @@ $ uv add --no-editable ./path/foo
|
|||
uv allows dependencies to be "virtual", in which the dependency itself is not installed as a
|
||||
[package](./config.md#project-packaging), but its dependencies are.
|
||||
|
||||
By default, only workspace members without build systems declared are virtual.
|
||||
By default, dependencies are never virtual.
|
||||
|
||||
A dependency with a [`path` source](#path) is not virtual unless it explicitly sets
|
||||
A dependency with a [`path` source](#path) can be virtual if it explicitly sets
|
||||
[`tool.uv.package = false`](../../reference/settings.md#package). Unlike working _in_ the dependent
|
||||
project with uv, the package will be built even if a [build system](./config.md#build-systems) is
|
||||
not declared.
|
||||
|
|
@ -825,8 +825,8 @@ dependencies = ["bar"]
|
|||
bar = { path = "../projects/bar", package = false }
|
||||
```
|
||||
|
||||
Similarly, if a dependency sets `tool.uv.package = false`, it can be overridden by declaring
|
||||
`package = true` on the source:
|
||||
If a dependency sets `tool.uv.package = false`, it can be overridden by declaring `package = true`
|
||||
on the source:
|
||||
|
||||
```toml title="pyproject.toml"
|
||||
[project]
|
||||
|
|
@ -836,6 +836,52 @@ dependencies = ["bar"]
|
|||
bar = { path = "../projects/bar", package = true }
|
||||
```
|
||||
|
||||
Similarly, a dependency with a [`workspace` source](#workspace-member) can be virtual if it
|
||||
explicitly sets [`tool.uv.package = false`](../../reference/settings.md#package). The workspace
|
||||
member will be built even if a [build system](./config.md#build-systems) is not declared.
|
||||
|
||||
Workspace members that are _not_ dependencies can be virtual by default, e.g., if the parent
|
||||
`pyproject.toml` is:
|
||||
|
||||
```toml title="pyproject.toml"
|
||||
[project]
|
||||
name = "parent"
|
||||
version = "1.0.0"
|
||||
dependencies = []
|
||||
|
||||
[tool.uv.workspace]
|
||||
members = ["child"]
|
||||
```
|
||||
|
||||
And the child `pyproject.toml` excluded a build system:
|
||||
|
||||
```toml title="pyproject.toml"
|
||||
[project]
|
||||
name = "child"
|
||||
version = "1.0.0"
|
||||
dependencies = ["anyio"]
|
||||
```
|
||||
|
||||
Then the `child` workspace member would not be installed, but the transitive dependency `anyio`
|
||||
would be.
|
||||
|
||||
In contrast, if the parent declared a dependency on `child`:
|
||||
|
||||
```toml title="pyproject.toml"
|
||||
[project]
|
||||
name = "parent"
|
||||
version = "1.0.0"
|
||||
dependencies = ["child"]
|
||||
|
||||
[tool.uv.sources]
|
||||
child = { workspace = true }
|
||||
|
||||
[tool.uv.workspace]
|
||||
members = ["child"]
|
||||
```
|
||||
|
||||
Then `child` would be built and installed.
|
||||
|
||||
## Dependency specifiers
|
||||
|
||||
uv uses standard
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue