mirror of
https://github.com/astral-sh/uv.git
synced 2025-10-28 02:40:11 +00:00
Allow editable = false for workspace sources (#15708)
## Summary This ended up being a bit more complex, similar to `package = false`, because we need to understand the editable status _globally_ across the workspace based on the packages that depend on it. Closes https://github.com/astral-sh/uv/issues/15686.
This commit is contained in:
parent
97777cda66
commit
e84c9231aa
14 changed files with 713 additions and 132 deletions
|
|
@ -306,22 +306,24 @@ impl LoweredRequirement {
|
|||
},
|
||||
url,
|
||||
}
|
||||
} else if member
|
||||
.pyproject_toml()
|
||||
.is_package(!workspace.is_required_member(&requirement.name))
|
||||
{
|
||||
RequirementSource::Directory {
|
||||
install_path: install_path.into_boxed_path(),
|
||||
url,
|
||||
editable: Some(true),
|
||||
r#virtual: Some(false),
|
||||
}
|
||||
} else {
|
||||
RequirementSource::Directory {
|
||||
install_path: install_path.into_boxed_path(),
|
||||
url,
|
||||
editable: Some(false),
|
||||
r#virtual: Some(true),
|
||||
let value = workspace.required_members().get(&requirement.name);
|
||||
let is_required_member = value.is_some();
|
||||
let editability = value.copied().flatten();
|
||||
if member.pyproject_toml().is_package(!is_required_member) {
|
||||
RequirementSource::Directory {
|
||||
install_path: install_path.into_boxed_path(),
|
||||
url,
|
||||
editable: Some(editability.unwrap_or(true)),
|
||||
r#virtual: Some(false),
|
||||
}
|
||||
} else {
|
||||
RequirementSource::Directory {
|
||||
install_path: install_path.into_boxed_path(),
|
||||
url,
|
||||
editable: Some(false),
|
||||
r#virtual: Some(true),
|
||||
}
|
||||
}
|
||||
};
|
||||
(source, marker)
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ use uv_pypi_types::{
|
|||
use uv_redacted::DisplaySafeUrl;
|
||||
use uv_small_str::SmallString;
|
||||
use uv_types::{BuildContext, HashStrategy};
|
||||
use uv_workspace::WorkspaceMember;
|
||||
use uv_workspace::{Editability, WorkspaceMember};
|
||||
|
||||
use crate::fork_strategy::ForkStrategy;
|
||||
pub(crate) use crate::lock::export::PylockTomlPackage;
|
||||
|
|
@ -1443,7 +1443,7 @@ impl Lock {
|
|||
root: &Path,
|
||||
packages: &BTreeMap<PackageName, WorkspaceMember>,
|
||||
members: &[PackageName],
|
||||
required_members: &BTreeSet<PackageName>,
|
||||
required_members: &BTreeMap<PackageName, Editability>,
|
||||
requirements: &[Requirement],
|
||||
constraints: &[Requirement],
|
||||
overrides: &[Requirement],
|
||||
|
|
@ -1471,17 +1471,37 @@ 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 {
|
||||
// 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()
|
||||
.flatten()
|
||||
.map(|package| matches!(package.id.source, Source::Virtual(_)));
|
||||
if actual != Some(expected) {
|
||||
return Ok(SatisfiesResult::MismatchedVirtual(name.clone(), expected));
|
||||
let source = self.find_by_name(name).ok().flatten();
|
||||
|
||||
// Determine whether the member was required by any other member.
|
||||
let value = required_members.get(name);
|
||||
let is_required_member = value.is_some();
|
||||
let editability = value.copied().flatten();
|
||||
|
||||
// Verify that the member is virtual (or not).
|
||||
let expected_virtual = !member.pyproject_toml().is_package(!is_required_member);
|
||||
let actual_virtual =
|
||||
source.map(|package| matches!(package.id.source, Source::Virtual(..)));
|
||||
if actual_virtual != Some(expected_virtual) {
|
||||
return Ok(SatisfiesResult::MismatchedVirtual(
|
||||
name.clone(),
|
||||
expected_virtual,
|
||||
));
|
||||
}
|
||||
|
||||
// Verify that the member is editable (or not).
|
||||
let expected_editable = if expected_virtual {
|
||||
false
|
||||
} else {
|
||||
editability.unwrap_or(true)
|
||||
};
|
||||
let actual_editable =
|
||||
source.map(|package| matches!(package.id.source, Source::Editable(..)));
|
||||
if actual_editable != Some(expected_editable) {
|
||||
return Ok(SatisfiesResult::MismatchedEditable(
|
||||
name.clone(),
|
||||
expected_editable,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1995,6 +2015,8 @@ pub enum SatisfiesResult<'lock> {
|
|||
MismatchedMembers(BTreeSet<PackageName>, &'lock BTreeSet<PackageName>),
|
||||
/// A workspace member switched from virtual to non-virtual or vice versa.
|
||||
MismatchedVirtual(PackageName, bool),
|
||||
/// A workspace member switched from editable to non-editable or vice versa.
|
||||
MismatchedEditable(PackageName, bool),
|
||||
/// A source tree switched from dynamic to non-dynamic or vice versa.
|
||||
MismatchedDynamic(&'lock PackageName, bool),
|
||||
/// The lockfile uses a different set of version for its workspace members.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
pub use workspace::{
|
||||
DiscoveryOptions, MemberDiscovery, ProjectWorkspace, RequiresPythonSources, VirtualProject,
|
||||
Workspace, WorkspaceCache, WorkspaceError, WorkspaceMember,
|
||||
DiscoveryOptions, Editability, MemberDiscovery, ProjectWorkspace, RequiresPythonSources,
|
||||
VirtualProject, Workspace, WorkspaceCache, WorkspaceError, WorkspaceMember,
|
||||
};
|
||||
|
||||
pub mod dependency_groups;
|
||||
|
|
|
|||
|
|
@ -1161,6 +1161,8 @@ pub enum Source {
|
|||
/// When set to `false`, the package will be fetched from the remote index, rather than
|
||||
/// included as a workspace package.
|
||||
workspace: bool,
|
||||
/// Whether the package should be installed as editable. Defaults to `true`.
|
||||
editable: Option<bool>,
|
||||
#[serde(
|
||||
skip_serializing_if = "uv_pep508::marker::ser::is_empty",
|
||||
serialize_with = "uv_pep508::marker::ser::serialize",
|
||||
|
|
@ -1498,11 +1500,6 @@ impl<'de> Deserialize<'de> for Source {
|
|||
"cannot specify both `workspace` and `branch`",
|
||||
));
|
||||
}
|
||||
if editable.is_some() {
|
||||
return Err(serde::de::Error::custom(
|
||||
"cannot specify both `workspace` and `editable`",
|
||||
));
|
||||
}
|
||||
if package.is_some() {
|
||||
return Err(serde::de::Error::custom(
|
||||
"cannot specify both `workspace` and `package`",
|
||||
|
|
@ -1511,6 +1508,7 @@ impl<'de> Deserialize<'de> for Source {
|
|||
|
||||
return Ok(Self::Workspace {
|
||||
workspace,
|
||||
editable,
|
||||
marker,
|
||||
extra,
|
||||
group,
|
||||
|
|
@ -1550,10 +1548,6 @@ pub enum SourceError {
|
|||
"`{0}` did not resolve to a local directory, but the `--editable` flag was provided. Editable installs are only supported for local directories."
|
||||
)]
|
||||
UnusedEditable(String),
|
||||
#[error(
|
||||
"Workspace dependency `{0}` was marked as `--no-editable`, but workspace dependencies are always added in editable mode. Pass `--no-editable` to `uv sync` or `uv run` to install workspace dependencies in non-editable mode."
|
||||
)]
|
||||
UnusedNoEditable(String),
|
||||
#[error("Failed to resolve absolute path")]
|
||||
Absolute(#[from] std::io::Error),
|
||||
#[error("Path contains invalid characters: `{}`", _0.display())]
|
||||
|
|
@ -1623,13 +1617,8 @@ impl Source {
|
|||
}
|
||||
}
|
||||
|
||||
if workspace {
|
||||
// If a workspace source is added with `--no-editable`, error.
|
||||
if editable == Some(false) {
|
||||
return Err(SourceError::UnusedNoEditable(name.to_string()));
|
||||
}
|
||||
} else {
|
||||
// If we resolved a non-path source, and user specified an `--editable` flag, error.
|
||||
// If we resolved a non-path source, and user specified an `--editable` flag, error.
|
||||
if !workspace {
|
||||
if !matches!(source, RequirementSource::Directory { .. }) {
|
||||
if editable == Some(true) {
|
||||
return Err(SourceError::UnusedEditable(name.to_string()));
|
||||
|
|
@ -1643,6 +1632,7 @@ impl Source {
|
|||
RequirementSource::Registry { .. } | RequirementSource::Directory { .. } => {
|
||||
Ok(Some(Self::Workspace {
|
||||
workspace: true,
|
||||
editable,
|
||||
marker: MarkerTree::TRUE,
|
||||
extra: None,
|
||||
group: None,
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ pub enum WorkspaceError {
|
|||
NonWorkspace(PathBuf),
|
||||
#[error("Nested workspaces are not supported, but workspace member (`{}`) has a `uv.workspace` table", _0.simplified_display())]
|
||||
NestedWorkspace(PathBuf),
|
||||
#[error("Two workspace members are both named: `{name}`: `{}` and `{}`", first.simplified_display(), second.simplified_display())]
|
||||
#[error("Two workspace members are both named `{name}`: `{}` and `{}`", first.simplified_display(), second.simplified_display())]
|
||||
DuplicatePackage {
|
||||
name: PackageName,
|
||||
first: PathBuf,
|
||||
|
|
@ -64,6 +64,11 @@ pub enum WorkspaceError {
|
|||
},
|
||||
#[error("pyproject.toml section is declared as dynamic, but must be static: `{0}`")]
|
||||
DynamicNotAllowed(&'static str),
|
||||
#[error(
|
||||
"Workspace member `{}` was requested as both `editable = true` and `editable = false`",
|
||||
_0
|
||||
)]
|
||||
EditableConflict(PackageName),
|
||||
#[error("Failed to find directories for glob: `{0}`")]
|
||||
Pattern(String, #[source] PatternError),
|
||||
// Syntax and other errors.
|
||||
|
|
@ -98,6 +103,8 @@ pub struct DiscoveryOptions {
|
|||
|
||||
pub type RequiresPythonSources = BTreeMap<(PackageName, Option<GroupName>), VersionSpecifiers>;
|
||||
|
||||
pub type Editability = Option<bool>;
|
||||
|
||||
/// A workspace, consisting of a root directory and members. See [`ProjectWorkspace`].
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(test, derive(serde::Serialize))]
|
||||
|
|
@ -109,8 +116,9 @@ 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 workspace members that are required by other members, and whether they were requested
|
||||
/// as editable.
|
||||
required_members: BTreeMap<PackageName, Editability>,
|
||||
/// The sources table from the workspace `pyproject.toml`.
|
||||
///
|
||||
/// This table is overridden by the project sources.
|
||||
|
|
@ -255,15 +263,16 @@ impl Workspace {
|
|||
/// Set the [`ProjectWorkspace`] for a given workspace member.
|
||||
///
|
||||
/// Assumes that the project name is unchanged in the updated [`PyProjectToml`].
|
||||
#[must_use]
|
||||
pub fn with_pyproject_toml(
|
||||
self,
|
||||
package_name: &PackageName,
|
||||
pyproject_toml: PyProjectToml,
|
||||
) -> Option<Self> {
|
||||
) -> Result<Option<Self>, WorkspaceError> {
|
||||
let mut packages = self.packages;
|
||||
|
||||
let member = Arc::make_mut(&mut packages).get_mut(package_name)?;
|
||||
let Some(member) = Arc::make_mut(&mut packages).get_mut(package_name) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
if member.root == self.install_path {
|
||||
// If the member is also the workspace root, update _both_ the member entry and the
|
||||
|
|
@ -287,28 +296,28 @@ impl Workspace {
|
|||
&packages,
|
||||
&workspace_sources,
|
||||
&workspace_pyproject_toml,
|
||||
);
|
||||
)?;
|
||||
|
||||
Some(Self {
|
||||
Ok(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;
|
||||
|
||||
// Recompute required_members with the updated member data
|
||||
let required_members =
|
||||
Self::collect_required_members(&packages, &self.sources, &self.pyproject_toml);
|
||||
Self::collect_required_members(&packages, &self.sources, &self.pyproject_toml)?;
|
||||
|
||||
Some(Self {
|
||||
Ok(Some(Self {
|
||||
packages,
|
||||
required_members,
|
||||
..self
|
||||
})
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -337,7 +346,13 @@ impl Workspace {
|
|||
{
|
||||
RequirementSource::Directory {
|
||||
install_path: member.root.clone().into_boxed_path(),
|
||||
editable: Some(true),
|
||||
editable: Some(
|
||||
self.required_members
|
||||
.get(name)
|
||||
.copied()
|
||||
.flatten()
|
||||
.unwrap_or(true),
|
||||
),
|
||||
r#virtual: Some(false),
|
||||
url,
|
||||
}
|
||||
|
|
@ -355,11 +370,12 @@ impl Workspace {
|
|||
}
|
||||
|
||||
/// The workspace members that are required my another member of the workspace.
|
||||
pub fn required_members(&self) -> &BTreeSet<PackageName> {
|
||||
pub fn required_members(&self) -> &BTreeMap<PackageName, Editability> {
|
||||
&self.required_members
|
||||
}
|
||||
|
||||
/// Compute the workspace members that are required by another member of the workspace.
|
||||
/// Compute the workspace members that are required by another member of the workspace, and
|
||||
/// determine whether they should be installed as editable or non-editable.
|
||||
///
|
||||
/// 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
|
||||
|
|
@ -368,8 +384,10 @@ impl Workspace {
|
|||
packages: &BTreeMap<PackageName, WorkspaceMember>,
|
||||
sources: &BTreeMap<PackageName, Sources>,
|
||||
pyproject_toml: &PyProjectToml,
|
||||
) -> BTreeSet<PackageName> {
|
||||
sources
|
||||
) -> Result<BTreeMap<PackageName, Editability>, WorkspaceError> {
|
||||
let mut required_members = BTreeMap::new();
|
||||
|
||||
for (package, sources) in sources
|
||||
.iter()
|
||||
.filter(|(name, _)| {
|
||||
pyproject_toml
|
||||
|
|
@ -396,18 +414,29 @@ impl Workspace {
|
|||
})
|
||||
.flatten(),
|
||||
)
|
||||
.filter_map(|(package, sources)| {
|
||||
sources
|
||||
.iter()
|
||||
.any(|source| matches!(source, Source::Workspace { .. }))
|
||||
.then_some(package.clone())
|
||||
})
|
||||
.collect()
|
||||
{
|
||||
for source in sources.iter() {
|
||||
let Source::Workspace { editable, .. } = &source else {
|
||||
continue;
|
||||
};
|
||||
let existing = required_members.insert(package.clone(), *editable);
|
||||
if let Some(Some(existing)) = existing {
|
||||
if let Some(editable) = editable {
|
||||
// If there are conflicting `editable` values, raise an error.
|
||||
if existing != *editable {
|
||||
return Err(WorkspaceError::EditableConflict(package.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(required_members)
|
||||
}
|
||||
|
||||
/// Whether a given workspace member is required by another member.
|
||||
pub fn is_required_member(&self, name: &PackageName) -> bool {
|
||||
self.required_members().contains(name)
|
||||
self.required_members().contains_key(name)
|
||||
}
|
||||
|
||||
/// Returns the set of all workspace member dependency groups.
|
||||
|
|
@ -441,18 +470,19 @@ impl Workspace {
|
|||
return None;
|
||||
}
|
||||
|
||||
let value = self.required_members.get(name);
|
||||
let is_required_member = value.is_some();
|
||||
let editability = value.copied().flatten();
|
||||
|
||||
Some(Requirement {
|
||||
name: member.pyproject_toml.project.as_ref()?.name.clone(),
|
||||
extras: Box::new([]),
|
||||
groups: groups.into_boxed_slice(),
|
||||
marker: MarkerTree::TRUE,
|
||||
source: if member
|
||||
.pyproject_toml()
|
||||
.is_package(!self.is_required_member(name))
|
||||
{
|
||||
source: if member.pyproject_toml().is_package(!is_required_member) {
|
||||
RequirementSource::Directory {
|
||||
install_path: member.root.clone().into_boxed_path(),
|
||||
editable: Some(true),
|
||||
editable: Some(editability.unwrap_or(true)),
|
||||
r#virtual: Some(false),
|
||||
url,
|
||||
}
|
||||
|
|
@ -831,7 +861,7 @@ impl Workspace {
|
|||
&workspace_members,
|
||||
&workspace_sources,
|
||||
&workspace_pyproject_toml,
|
||||
);
|
||||
)?;
|
||||
|
||||
Ok(Self {
|
||||
install_path: workspace_root,
|
||||
|
|
@ -1250,14 +1280,17 @@ impl ProjectWorkspace {
|
|||
/// Set the `pyproject.toml` for the current project.
|
||||
///
|
||||
/// Assumes that the project name is unchanged in the updated [`PyProjectToml`].
|
||||
#[must_use]
|
||||
pub fn with_pyproject_toml(self, pyproject_toml: PyProjectToml) -> Option<Self> {
|
||||
Some(Self {
|
||||
workspace: self
|
||||
.workspace
|
||||
.with_pyproject_toml(&self.project_name, pyproject_toml)?,
|
||||
..self
|
||||
})
|
||||
pub fn with_pyproject_toml(
|
||||
self,
|
||||
pyproject_toml: PyProjectToml,
|
||||
) -> Result<Option<Self>, WorkspaceError> {
|
||||
let Some(workspace) = self
|
||||
.workspace
|
||||
.with_pyproject_toml(&self.project_name, pyproject_toml)?
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
Ok(Some(Self { workspace, ..self }))
|
||||
}
|
||||
|
||||
/// Find the workspace for a project.
|
||||
|
|
@ -1325,7 +1358,7 @@ impl ProjectWorkspace {
|
|||
¤t_project_as_members,
|
||||
&workspace_sources,
|
||||
project_pyproject_toml,
|
||||
);
|
||||
)?;
|
||||
|
||||
return Ok(Self {
|
||||
project_root: project_path.clone(),
|
||||
|
|
@ -1647,11 +1680,16 @@ impl VirtualProject {
|
|||
/// Set the `pyproject.toml` for the current project.
|
||||
///
|
||||
/// Assumes that the project name is unchanged in the updated [`PyProjectToml`].
|
||||
#[must_use]
|
||||
pub fn with_pyproject_toml(self, pyproject_toml: PyProjectToml) -> Option<Self> {
|
||||
match self {
|
||||
pub fn with_pyproject_toml(
|
||||
self,
|
||||
pyproject_toml: PyProjectToml,
|
||||
) -> Result<Option<Self>, WorkspaceError> {
|
||||
Ok(match self {
|
||||
Self::Project(project) => {
|
||||
Some(Self::Project(project.with_pyproject_toml(pyproject_toml)?))
|
||||
let Some(project) = project.with_pyproject_toml(pyproject_toml)? else {
|
||||
return Ok(None);
|
||||
};
|
||||
Some(Self::Project(project))
|
||||
}
|
||||
Self::NonProject(workspace) => {
|
||||
// If this is a non-project workspace root, then by definition the root isn't a
|
||||
|
|
@ -1661,7 +1699,7 @@ impl VirtualProject {
|
|||
..workspace.clone()
|
||||
}))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Return the root of the project.
|
||||
|
|
@ -1788,7 +1826,7 @@ mod tests {
|
|||
"pyproject_toml": "[PYPROJECT_TOML]"
|
||||
}
|
||||
},
|
||||
"required_members": [],
|
||||
"required_members": {},
|
||||
"sources": {},
|
||||
"indexes": [],
|
||||
"pyproject_toml": {
|
||||
|
|
@ -1842,7 +1880,7 @@ mod tests {
|
|||
"pyproject_toml": "[PYPROJECT_TOML]"
|
||||
}
|
||||
},
|
||||
"required_members": [],
|
||||
"required_members": {},
|
||||
"sources": {},
|
||||
"indexes": [],
|
||||
"pyproject_toml": {
|
||||
|
|
@ -1923,14 +1961,15 @@ mod tests {
|
|||
"pyproject_toml": "[PYPROJECT_TOML]"
|
||||
}
|
||||
},
|
||||
"required_members": [
|
||||
"bird-feeder",
|
||||
"seeds"
|
||||
],
|
||||
"required_members": {
|
||||
"bird-feeder": null,
|
||||
"seeds": null
|
||||
},
|
||||
"sources": {
|
||||
"bird-feeder": [
|
||||
{
|
||||
"workspace": true,
|
||||
"editable": null,
|
||||
"extra": null,
|
||||
"group": null
|
||||
}
|
||||
|
|
@ -1954,6 +1993,7 @@ mod tests {
|
|||
"bird-feeder": [
|
||||
{
|
||||
"workspace": true,
|
||||
"editable": null,
|
||||
"extra": null,
|
||||
"group": null
|
||||
}
|
||||
|
|
@ -2048,10 +2088,10 @@ mod tests {
|
|||
"pyproject_toml": "[PYPROJECT_TOML]"
|
||||
}
|
||||
},
|
||||
"required_members": [
|
||||
"bird-feeder",
|
||||
"seeds"
|
||||
],
|
||||
"required_members": {
|
||||
"bird-feeder": null,
|
||||
"seeds": null
|
||||
},
|
||||
"sources": {},
|
||||
"indexes": [],
|
||||
"pyproject_toml": {
|
||||
|
|
@ -2119,7 +2159,7 @@ mod tests {
|
|||
"pyproject_toml": "[PYPROJECT_TOML]"
|
||||
}
|
||||
},
|
||||
"required_members": [],
|
||||
"required_members": {},
|
||||
"sources": {},
|
||||
"indexes": [],
|
||||
"pyproject_toml": {
|
||||
|
|
@ -2254,7 +2294,7 @@ mod tests {
|
|||
"pyproject_toml": "[PYPROJECT_TOML]"
|
||||
}
|
||||
},
|
||||
"required_members": [],
|
||||
"required_members": {},
|
||||
"sources": {},
|
||||
"indexes": [],
|
||||
"pyproject_toml": {
|
||||
|
|
@ -2362,7 +2402,7 @@ mod tests {
|
|||
"pyproject_toml": "[PYPROJECT_TOML]"
|
||||
}
|
||||
},
|
||||
"required_members": [],
|
||||
"required_members": {},
|
||||
"sources": {},
|
||||
"indexes": [],
|
||||
"pyproject_toml": {
|
||||
|
|
@ -2484,7 +2524,7 @@ mod tests {
|
|||
"pyproject_toml": "[PYPROJECT_TOML]"
|
||||
}
|
||||
},
|
||||
"required_members": [],
|
||||
"required_members": {},
|
||||
"sources": {},
|
||||
"indexes": [],
|
||||
"pyproject_toml": {
|
||||
|
|
@ -2580,7 +2620,7 @@ mod tests {
|
|||
"pyproject_toml": "[PYPROJECT_TOML]"
|
||||
}
|
||||
},
|
||||
"required_members": [],
|
||||
"required_members": {},
|
||||
"sources": {},
|
||||
"indexes": [],
|
||||
"pyproject_toml": {
|
||||
|
|
@ -2771,7 +2811,7 @@ bar = ["b"]
|
|||
insta::with_settings!({filters => filters}, {
|
||||
assert_snapshot!(
|
||||
error,
|
||||
@"Two workspace members are both named: `seeds`: `[ROOT]/packages/seeds` and `[ROOT]/packages/seeds2`");
|
||||
@"Two workspace members are both named `seeds`: `[ROOT]/packages/seeds` and `[ROOT]/packages/seeds2`");
|
||||
});
|
||||
|
||||
Ok(())
|
||||
|
|
|
|||
|
|
@ -1391,7 +1391,7 @@ impl AddTarget {
|
|||
let project = project
|
||||
.with_pyproject_toml(
|
||||
toml::from_str(content).map_err(ProjectError::PyprojectTomlParse)?,
|
||||
)
|
||||
)?
|
||||
.ok_or(ProjectError::PyprojectTomlUpdate)?;
|
||||
Ok(Self::Project(project, venv))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ use uv_scripts::Pep723Script;
|
|||
use uv_settings::PythonInstallMirrors;
|
||||
use uv_types::{BuildContext, BuildIsolation, EmptyInstalledPackages, HashStrategy};
|
||||
use uv_warnings::{warn_user, warn_user_once};
|
||||
use uv_workspace::{DiscoveryOptions, Workspace, WorkspaceCache, WorkspaceMember};
|
||||
use uv_workspace::{DiscoveryOptions, Editability, Workspace, WorkspaceCache, WorkspaceMember};
|
||||
|
||||
use crate::commands::pip::loggers::{DefaultResolveLogger, ResolveLogger, SummaryResolveLogger};
|
||||
use crate::commands::project::lock_target::LockTarget;
|
||||
|
|
@ -943,7 +943,7 @@ impl ValidatedLock {
|
|||
install_path: &Path,
|
||||
packages: &BTreeMap<PackageName, WorkspaceMember>,
|
||||
members: &[PackageName],
|
||||
required_members: &BTreeSet<PackageName>,
|
||||
required_members: &BTreeMap<PackageName, Editability>,
|
||||
requirements: &[Requirement],
|
||||
dependency_groups: &BTreeMap<GroupName, Vec<Requirement>>,
|
||||
constraints: &[Requirement],
|
||||
|
|
@ -1187,6 +1187,18 @@ impl ValidatedLock {
|
|||
);
|
||||
Ok(Self::Preferable(lock))
|
||||
}
|
||||
SatisfiesResult::MismatchedEditable(name, expected) => {
|
||||
if expected {
|
||||
debug!(
|
||||
"Resolving despite existing lockfile due to mismatched source: `{name}` (expected: `editable`)"
|
||||
);
|
||||
} else {
|
||||
debug!(
|
||||
"Resolving despite existing lockfile due to mismatched source: `{name}` (unexpected: `editable`)"
|
||||
);
|
||||
}
|
||||
Ok(Self::Preferable(lock))
|
||||
}
|
||||
SatisfiesResult::MismatchedVirtual(name, expected) => {
|
||||
if expected {
|
||||
debug!(
|
||||
|
|
@ -1194,7 +1206,7 @@ impl ValidatedLock {
|
|||
);
|
||||
} else {
|
||||
debug!(
|
||||
"Resolving despite existing lockfile due to mismatched source: `{name}` (expected: `editable`)"
|
||||
"Resolving despite existing lockfile due to mismatched source: `{name}` (unexpected: `virtual`)"
|
||||
);
|
||||
}
|
||||
Ok(Self::Preferable(lock))
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use itertools::Either;
|
||||
|
|
@ -12,7 +12,7 @@ use uv_pypi_types::{Conflicts, SupportedEnvironments, VerbatimParsedUrl};
|
|||
use uv_resolver::{Lock, LockVersion, VERSION};
|
||||
use uv_scripts::Pep723Script;
|
||||
use uv_workspace::dependency_groups::{DependencyGroupError, FlatDependencyGroup};
|
||||
use uv_workspace::{Workspace, WorkspaceMember};
|
||||
use uv_workspace::{Editability, Workspace, WorkspaceMember};
|
||||
|
||||
use crate::commands::project::{ProjectError, find_requires_python};
|
||||
|
||||
|
|
@ -156,11 +156,11 @@ 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> {
|
||||
pub(crate) fn required_members(self) -> &'lock BTreeMap<PackageName, Editability> {
|
||||
match self {
|
||||
Self::Workspace(workspace) => workspace.required_members(),
|
||||
Self::Script(_) => {
|
||||
static EMPTY: BTreeSet<PackageName> = BTreeSet::new();
|
||||
static EMPTY: BTreeMap<PackageName, Editability> = BTreeMap::new();
|
||||
&EMPTY
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -251,6 +251,9 @@ pub(crate) enum ProjectError {
|
|||
#[error(transparent)]
|
||||
Lowering(#[from] uv_distribution::LoweringError),
|
||||
|
||||
#[error(transparent)]
|
||||
Workspace(#[from] uv_workspace::WorkspaceError),
|
||||
|
||||
#[error(transparent)]
|
||||
PyprojectMut(#[from] uv_workspace::pyproject_mut::Error),
|
||||
|
||||
|
|
|
|||
|
|
@ -436,7 +436,7 @@ impl RemoveTarget {
|
|||
let project = project
|
||||
.with_pyproject_toml(
|
||||
toml::from_str(content).map_err(ProjectError::PyprojectTomlParse)?,
|
||||
)
|
||||
)?
|
||||
.ok_or(ProjectError::PyprojectTomlUpdate)?;
|
||||
Ok(Self::Project(project))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -392,7 +392,7 @@ fn update_project(
|
|||
|
||||
// Update the `pyproject.toml` in-memory.
|
||||
let project = project
|
||||
.with_pyproject_toml(toml::from_str(&content).map_err(ProjectError::PyprojectTomlParse)?)
|
||||
.with_pyproject_toml(toml::from_str(&content).map_err(ProjectError::PyprojectTomlParse)?)?
|
||||
.ok_or(ProjectError::PyprojectTomlUpdate)?;
|
||||
|
||||
Ok(project)
|
||||
|
|
|
|||
|
|
@ -2248,26 +2248,13 @@ fn add_workspace_editable() -> Result<()> {
|
|||
|
||||
let child1 = context.temp_dir.join("child1");
|
||||
|
||||
// `--no-editable` should error.
|
||||
// `--no-editable` should add `editable = false`.
|
||||
let mut add_cmd = context.add();
|
||||
add_cmd
|
||||
.arg("child2")
|
||||
.arg("--no-editable")
|
||||
.current_dir(&child1);
|
||||
|
||||
uv_snapshot!(context.filters(), add_cmd, @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: Workspace dependency `child2` was marked as `--no-editable`, but workspace dependencies are always added in editable mode. Pass `--no-editable` to `uv sync` or `uv run` to install workspace dependencies in non-editable mode.
|
||||
"###);
|
||||
|
||||
// `--editable` should not.
|
||||
let mut add_cmd = context.add();
|
||||
add_cmd.arg("child2").arg("--editable").current_dir(&child1);
|
||||
|
||||
uv_snapshot!(context.filters(), add_cmd, @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
|
|
@ -2301,7 +2288,50 @@ fn add_workspace_editable() -> Result<()> {
|
|||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.uv.sources]
|
||||
child2 = { workspace = true }
|
||||
child2 = { workspace = true, editable = false }
|
||||
"#
|
||||
);
|
||||
});
|
||||
|
||||
// `--editable` should not.
|
||||
let mut add_cmd = context.add();
|
||||
add_cmd.arg("child2").arg("--editable").current_dir(&child1);
|
||||
|
||||
uv_snapshot!(context.filters(), add_cmd, @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 3 packages in [TIME]
|
||||
Prepared 2 packages in [TIME]
|
||||
Uninstalled 2 packages in [TIME]
|
||||
Installed 2 packages in [TIME]
|
||||
~ child1==0.1.0 (from file://[TEMP_DIR]/child1)
|
||||
~ child2==0.1.0 (from file://[TEMP_DIR]/child2)
|
||||
");
|
||||
|
||||
let pyproject_toml = fs_err::read_to_string(child1.join("pyproject.toml"))?;
|
||||
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
assert_snapshot!(
|
||||
pyproject_toml, @r#"
|
||||
[project]
|
||||
name = "child1"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"child2",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.uv.sources]
|
||||
child2 = { workspace = true, editable = true }
|
||||
"#
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -13599,3 +13599,478 @@ fn sync_extra_build_dependencies_cache() -> Result<()> {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(windows))]
|
||||
fn toggle_workspace_editable() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
let child = context.temp_dir.child("child");
|
||||
let pyproject_toml = child.child("pyproject.toml");
|
||||
pyproject_toml.write_str(
|
||||
r#"
|
||||
[project]
|
||||
name = "child"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = ["iniconfig>=1"]
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
"#,
|
||||
)?;
|
||||
child
|
||||
.child("src")
|
||||
.child("child")
|
||||
.child("__init__.py")
|
||||
.touch()?;
|
||||
|
||||
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 = ["child"]
|
||||
|
||||
[tool.uv.workspace]
|
||||
members = ["child"]
|
||||
|
||||
[tool.uv.sources]
|
||||
child = { workspace = true }
|
||||
"#,
|
||||
)?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.sync(), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 3 packages in [TIME]
|
||||
Prepared 2 packages in [TIME]
|
||||
Installed 2 packages in [TIME]
|
||||
+ child==0.1.0 (from file://[TEMP_DIR]/child)
|
||||
+ iniconfig==2.0.0
|
||||
");
|
||||
|
||||
let lock = context.read("uv.lock");
|
||||
|
||||
// The child should be editable by default.
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
assert_snapshot!(
|
||||
lock, @r#"
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[options]
|
||||
exclude-newer = "2024-03-25T00:00:00Z"
|
||||
|
||||
[manifest]
|
||||
members = [
|
||||
"child",
|
||||
"project",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "child"
|
||||
version = "0.1.0"
|
||||
source = { editable = "child" }
|
||||
dependencies = [
|
||||
{ name = "iniconfig" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [{ name = "iniconfig", specifier = ">=1" }]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646, upload-time = "2023-01-07T11:08:11.254Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892, upload-time = "2023-01-07T11:08:09.864Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "child" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [{ name = "child", editable = "child" }]
|
||||
"#
|
||||
);
|
||||
});
|
||||
|
||||
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 = ["child"]
|
||||
|
||||
[tool.uv.workspace]
|
||||
members = ["child"]
|
||||
|
||||
[tool.uv.sources]
|
||||
child = { workspace = true, editable = false }
|
||||
"#,
|
||||
)?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.sync(), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 3 packages in [TIME]
|
||||
Prepared 1 package in [TIME]
|
||||
Uninstalled 1 package in [TIME]
|
||||
Installed 1 package in [TIME]
|
||||
~ child==0.1.0 (from file://[TEMP_DIR]/child)
|
||||
");
|
||||
|
||||
let lock = context.read("uv.lock");
|
||||
|
||||
// The child should be editable by default.
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
assert_snapshot!(
|
||||
lock, @r#"
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[options]
|
||||
exclude-newer = "2024-03-25T00:00:00Z"
|
||||
|
||||
[manifest]
|
||||
members = [
|
||||
"child",
|
||||
"project",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "child"
|
||||
version = "0.1.0"
|
||||
source = { directory = "child" }
|
||||
dependencies = [
|
||||
{ name = "iniconfig" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [{ name = "iniconfig", specifier = ">=1" }]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646, upload-time = "2023-01-07T11:08:11.254Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892, upload-time = "2023-01-07T11:08:09.864Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "child" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [{ name = "child", directory = "child" }]
|
||||
"#
|
||||
);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(windows))]
|
||||
fn workspace_editable_conflict() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
let child1 = context.temp_dir.child("child1");
|
||||
let pyproject_toml = child1.child("pyproject.toml");
|
||||
pyproject_toml.write_str(
|
||||
r#"
|
||||
[project]
|
||||
name = "child1"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = ["iniconfig>=1"]
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
"#,
|
||||
)?;
|
||||
child1
|
||||
.child("src")
|
||||
.child("child1")
|
||||
.child("__init__.py")
|
||||
.touch()?;
|
||||
|
||||
let child2 = context.temp_dir.child("child2");
|
||||
let pyproject_toml = child2.child("pyproject.toml");
|
||||
pyproject_toml.write_str(
|
||||
r#"
|
||||
[project]
|
||||
name = "child2"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = ["child1"]
|
||||
|
||||
[tool.uv.sources]
|
||||
child1 = { workspace = true }
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
"#,
|
||||
)?;
|
||||
child2
|
||||
.child("src")
|
||||
.child("child2")
|
||||
.child("__init__.py")
|
||||
.touch()?;
|
||||
|
||||
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 = ["child1"]
|
||||
|
||||
[tool.uv.workspace]
|
||||
members = ["child1", "child2"]
|
||||
|
||||
[tool.uv.sources]
|
||||
child1 = { workspace = true, editable = true }
|
||||
"#,
|
||||
)?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.sync(), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 4 packages in [TIME]
|
||||
Prepared 2 packages in [TIME]
|
||||
Installed 2 packages in [TIME]
|
||||
+ child1==0.1.0 (from file://[TEMP_DIR]/child1)
|
||||
+ iniconfig==2.0.0
|
||||
");
|
||||
|
||||
let lock = context.read("uv.lock");
|
||||
|
||||
// If one member declares `editable = true`, and the other omits `editable`, use editable.
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
assert_snapshot!(
|
||||
lock, @r#"
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[options]
|
||||
exclude-newer = "2024-03-25T00:00:00Z"
|
||||
|
||||
[manifest]
|
||||
members = [
|
||||
"child1",
|
||||
"child2",
|
||||
"project",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "child1"
|
||||
version = "0.1.0"
|
||||
source = { editable = "child1" }
|
||||
dependencies = [
|
||||
{ name = "iniconfig" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [{ name = "iniconfig", specifier = ">=1" }]
|
||||
|
||||
[[package]]
|
||||
name = "child2"
|
||||
version = "0.1.0"
|
||||
source = { editable = "child2" }
|
||||
dependencies = [
|
||||
{ name = "child1" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [{ name = "child1", editable = "child1" }]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646, upload-time = "2023-01-07T11:08:11.254Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892, upload-time = "2023-01-07T11:08:09.864Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "child1" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [{ name = "child1", editable = "child1" }]
|
||||
"#
|
||||
);
|
||||
});
|
||||
|
||||
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 = ["child1"]
|
||||
|
||||
[tool.uv.workspace]
|
||||
members = ["child1", "child2"]
|
||||
|
||||
[tool.uv.sources]
|
||||
child1 = { workspace = true, editable = false }
|
||||
"#,
|
||||
)?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.sync(), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 4 packages in [TIME]
|
||||
Prepared 1 package in [TIME]
|
||||
Uninstalled 1 package in [TIME]
|
||||
Installed 1 package in [TIME]
|
||||
~ child1==0.1.0 (from file://[TEMP_DIR]/child1)
|
||||
");
|
||||
|
||||
let lock = context.read("uv.lock");
|
||||
|
||||
// If one member declares `editable = false`, and the other omits `editable`, use non-editable.
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
assert_snapshot!(
|
||||
lock, @r#"
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[options]
|
||||
exclude-newer = "2024-03-25T00:00:00Z"
|
||||
|
||||
[manifest]
|
||||
members = [
|
||||
"child1",
|
||||
"child2",
|
||||
"project",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "child1"
|
||||
version = "0.1.0"
|
||||
source = { directory = "child1" }
|
||||
dependencies = [
|
||||
{ name = "iniconfig" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [{ name = "iniconfig", specifier = ">=1" }]
|
||||
|
||||
[[package]]
|
||||
name = "child2"
|
||||
version = "0.1.0"
|
||||
source = { editable = "child2" }
|
||||
dependencies = [
|
||||
{ name = "child1" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [{ name = "child1", directory = "child1" }]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646, upload-time = "2023-01-07T11:08:11.254Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892, upload-time = "2023-01-07T11:08:09.864Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "child1" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [{ name = "child1", directory = "child1" }]
|
||||
"#
|
||||
);
|
||||
});
|
||||
|
||||
let child2 = context.temp_dir.child("child2");
|
||||
let pyproject_toml = child2.child("pyproject.toml");
|
||||
pyproject_toml.write_str(
|
||||
r#"
|
||||
[project]
|
||||
name = "child2"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = ["child1"]
|
||||
|
||||
[tool.uv.sources]
|
||||
child1 = { workspace = true, editable = true }
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
// If the `editable` declarations are conflicting, raise an error.
|
||||
uv_snapshot!(context.filters(), context.sync(), @r"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: Workspace member `child1` was requested as both `editable = true` and `editable = false`
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue