mirror of
https://github.com/astral-sh/uv.git
synced 2026-05-06 08:56:53 -04:00
Avoid installing tool workspace member dependencies as editable (#18891)
## Summary Right now, if you use `uv tool install`, we install the tool itself as non-editable, but if the tool is part of a workspace, then any workspace dependencies are (accidentally) installed as editable. This PR modifies the behavior such that those dependencies are installed as non-editable, unless `--editable` is provided, in which case the tool itself and any workspace dependencies respect `--editable`. Similar logic applies to `--with` and `--with-editable`. If the target is in a workspace, we propagate the no-editable and yes-editable flags (respectively) to its members. Closes https://github.com/astral-sh/uv/issues/16306
This commit is contained in:
@@ -135,7 +135,9 @@ mod resolver {
|
||||
ExcludeNewer, FlatIndex, InMemoryIndex, Manifest, OptionsBuilder, PythonRequirement,
|
||||
Resolver, ResolverEnvironment, ResolverOutput,
|
||||
};
|
||||
use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy};
|
||||
use uv_types::{
|
||||
BuildIsolation, EmptyInstalledPackages, HashStrategy, SourceTreeEditablePolicy,
|
||||
};
|
||||
use uv_workspace::WorkspaceCache;
|
||||
|
||||
static MARKERS: LazyLock<MarkerEnvironment> = LazyLock::new(|| {
|
||||
@@ -238,6 +240,7 @@ mod resolver {
|
||||
&hashes,
|
||||
exclude_newer,
|
||||
sources,
|
||||
SourceTreeEditablePolicy::Project,
|
||||
workspace_cache,
|
||||
concurrency.clone(),
|
||||
Preview::default(),
|
||||
|
||||
@@ -650,6 +650,7 @@ impl SourceBuild {
|
||||
install_path,
|
||||
locations,
|
||||
no_sources,
|
||||
true,
|
||||
workspace_cache,
|
||||
credentials_cache,
|
||||
)
|
||||
@@ -1095,6 +1096,9 @@ async fn create_pep517_build_environment(
|
||||
install_path,
|
||||
locations,
|
||||
&no_sources,
|
||||
build_context
|
||||
.source_tree_editable_policy()
|
||||
.workspace_member_editable(None),
|
||||
workspace_cache,
|
||||
credentials_cache,
|
||||
)
|
||||
|
||||
@@ -36,7 +36,7 @@ use uv_resolver::{
|
||||
};
|
||||
use uv_types::{
|
||||
AnyErrorBuild, BuildArena, BuildContext, BuildIsolation, BuildStack, EmptyInstalledPackages,
|
||||
HashStrategy, InFlight,
|
||||
HashStrategy, InFlight, SourceTreeEditablePolicy,
|
||||
};
|
||||
use uv_workspace::WorkspaceCache;
|
||||
|
||||
@@ -98,6 +98,7 @@ pub struct BuildDispatch<'a> {
|
||||
source_build_context: SourceBuildContext,
|
||||
build_extra_env_vars: FxHashMap<OsString, OsString>,
|
||||
sources: NoSources,
|
||||
source_tree_editable_policy: SourceTreeEditablePolicy,
|
||||
workspace_cache: WorkspaceCache,
|
||||
concurrency: Concurrency,
|
||||
preview: Preview,
|
||||
@@ -124,6 +125,7 @@ impl<'a> BuildDispatch<'a> {
|
||||
hasher: &'a HashStrategy,
|
||||
exclude_newer: ExcludeNewer,
|
||||
sources: NoSources,
|
||||
source_tree_editable_policy: SourceTreeEditablePolicy,
|
||||
workspace_cache: WorkspaceCache,
|
||||
concurrency: Concurrency,
|
||||
preview: Preview,
|
||||
@@ -150,6 +152,7 @@ impl<'a> BuildDispatch<'a> {
|
||||
source_build_context: SourceBuildContext::new(concurrency.builds_semaphore.clone()),
|
||||
build_extra_env_vars: FxHashMap::default(),
|
||||
sources,
|
||||
source_tree_editable_policy,
|
||||
workspace_cache,
|
||||
concurrency,
|
||||
preview,
|
||||
@@ -220,6 +223,10 @@ impl BuildContext for BuildDispatch<'_> {
|
||||
&self.sources
|
||||
}
|
||||
|
||||
fn source_tree_editable_policy(&self) -> SourceTreeEditablePolicy {
|
||||
self.source_tree_editable_policy
|
||||
}
|
||||
|
||||
fn locations(&self) -> &IndexLocations {
|
||||
self.index_locations
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ impl BuildRequires {
|
||||
install_path: &Path,
|
||||
locations: &IndexLocations,
|
||||
sources: &NoSources,
|
||||
editable: bool,
|
||||
cache: &WorkspaceCache,
|
||||
credentials_cache: &CredentialsCache,
|
||||
) -> Result<Self, MetadataError> {
|
||||
@@ -64,6 +65,7 @@ impl BuildRequires {
|
||||
&project_workspace,
|
||||
locations,
|
||||
sources,
|
||||
editable,
|
||||
credentials_cache,
|
||||
)
|
||||
}
|
||||
@@ -74,6 +76,7 @@ impl BuildRequires {
|
||||
project_workspace: &ProjectWorkspace,
|
||||
locations: &IndexLocations,
|
||||
sources: &NoSources,
|
||||
editable: bool,
|
||||
credentials_cache: &CredentialsCache,
|
||||
) -> Result<Self, MetadataError> {
|
||||
// Collect any `tool.uv.index` entries.
|
||||
@@ -132,6 +135,7 @@ impl BuildRequires {
|
||||
locations,
|
||||
project_workspace.workspace(),
|
||||
None,
|
||||
editable,
|
||||
credentials_cache,
|
||||
)
|
||||
.map(move |requirement| match requirement {
|
||||
@@ -206,6 +210,7 @@ impl BuildRequires {
|
||||
locations,
|
||||
workspace,
|
||||
None,
|
||||
true,
|
||||
credentials_cache,
|
||||
)
|
||||
.map(move |requirement| match requirement {
|
||||
@@ -294,6 +299,7 @@ impl LoweredExtraBuildDependencies {
|
||||
index_locations,
|
||||
workspace,
|
||||
None,
|
||||
true,
|
||||
credentials_cache,
|
||||
)
|
||||
.map(move |requirement| {
|
||||
|
||||
@@ -164,6 +164,7 @@ impl SourcedDependencyGroups {
|
||||
locations,
|
||||
project.workspace(),
|
||||
git_member,
|
||||
true,
|
||||
credentials_cache,
|
||||
)
|
||||
.map(move |requirement| match requirement {
|
||||
|
||||
@@ -46,6 +46,7 @@ impl LoweredRequirement {
|
||||
locations: &'data IndexLocations,
|
||||
workspace: &'data Workspace,
|
||||
git_member: Option<&'data GitWorkspaceMember<'data>>,
|
||||
editable: bool,
|
||||
credentials_cache: &'data CredentialsCache,
|
||||
) -> impl Iterator<Item = Result<Self, LoweringError>> + use<'data> + 'data {
|
||||
// Identify the source from the `tool.uv.sources` table.
|
||||
@@ -320,7 +321,7 @@ impl LoweredRequirement {
|
||||
RequirementSource::Directory {
|
||||
install_path: install_path.into_boxed_path(),
|
||||
url,
|
||||
editable: Some(editability.unwrap_or(true)),
|
||||
editable: Some(editability.unwrap_or(editable)),
|
||||
r#virtual: Some(false),
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -91,6 +91,7 @@ impl Metadata {
|
||||
git_source: Option<&GitWorkspaceMember<'_>>,
|
||||
locations: &IndexLocations,
|
||||
sources: NoSources,
|
||||
editable: bool,
|
||||
cache: &WorkspaceCache,
|
||||
credentials_cache: &CredentialsCache,
|
||||
) -> Result<Self, MetadataError> {
|
||||
@@ -113,6 +114,7 @@ impl Metadata {
|
||||
git_source,
|
||||
locations,
|
||||
sources,
|
||||
editable,
|
||||
cache,
|
||||
credentials_cache,
|
||||
)
|
||||
|
||||
@@ -48,6 +48,7 @@ impl RequiresDist {
|
||||
git_member: Option<&GitWorkspaceMember<'_>>,
|
||||
locations: &IndexLocations,
|
||||
sources: NoSources,
|
||||
editable: bool,
|
||||
cache: &WorkspaceCache,
|
||||
credentials_cache: &CredentialsCache,
|
||||
) -> Result<Self, MetadataError> {
|
||||
@@ -78,6 +79,7 @@ impl RequiresDist {
|
||||
git_member,
|
||||
locations,
|
||||
&sources,
|
||||
editable,
|
||||
credentials_cache,
|
||||
)
|
||||
}
|
||||
@@ -88,6 +90,7 @@ impl RequiresDist {
|
||||
git_member: Option<&GitWorkspaceMember<'_>>,
|
||||
locations: &IndexLocations,
|
||||
no_sources: &NoSources,
|
||||
editable: bool,
|
||||
credentials_cache: &CredentialsCache,
|
||||
) -> Result<Self, MetadataError> {
|
||||
// Collect any `tool.uv.index` entries.
|
||||
@@ -149,6 +152,7 @@ impl RequiresDist {
|
||||
locations,
|
||||
project_workspace.workspace(),
|
||||
git_member,
|
||||
editable,
|
||||
credentials_cache,
|
||||
)
|
||||
.map(move |requirement| match requirement {
|
||||
@@ -191,6 +195,7 @@ impl RequiresDist {
|
||||
locations,
|
||||
project_workspace.workspace(),
|
||||
git_member,
|
||||
editable,
|
||||
credentials_cache,
|
||||
)
|
||||
.map(move |requirement| match requirement {
|
||||
@@ -477,6 +482,7 @@ mod test {
|
||||
None,
|
||||
&IndexLocations::default(),
|
||||
&NoSources::default(),
|
||||
true,
|
||||
&CredentialsCache::new(),
|
||||
)?)
|
||||
}
|
||||
|
||||
@@ -1281,6 +1281,14 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
|
||||
return Err(Error::HashesNotSupportedSourceTree(source.to_string()));
|
||||
}
|
||||
|
||||
// Project-style resolution always lowers workspace members as editable. Tool-style
|
||||
// resolution preserves an explicit local requirement choice instead, defaulting implicit
|
||||
// workspace siblings to non-editable.
|
||||
let editable = self
|
||||
.build_context
|
||||
.source_tree_editable_policy()
|
||||
.workspace_member_editable(resource.editable);
|
||||
|
||||
// If the metadata is static, return it.
|
||||
let dynamic = match StaticMetadata::read(source, resource.install_path, None).await? {
|
||||
StaticMetadata::Some(metadata) => {
|
||||
@@ -1291,6 +1299,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
|
||||
None,
|
||||
self.build_context.locations(),
|
||||
self.build_context.sources().clone(),
|
||||
editable,
|
||||
self.build_context.workspace_cache(),
|
||||
credentials_cache,
|
||||
)
|
||||
@@ -1345,6 +1354,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
|
||||
None,
|
||||
self.build_context.locations(),
|
||||
self.build_context.sources().clone(),
|
||||
editable,
|
||||
self.build_context.workspace_cache(),
|
||||
credentials_cache,
|
||||
)
|
||||
@@ -1395,6 +1405,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
|
||||
None,
|
||||
self.build_context.locations(),
|
||||
self.build_context.sources().clone(),
|
||||
editable,
|
||||
self.build_context.workspace_cache(),
|
||||
credentials_cache,
|
||||
)
|
||||
@@ -1457,6 +1468,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
|
||||
None,
|
||||
self.build_context.locations(),
|
||||
self.build_context.sources().clone(),
|
||||
editable,
|
||||
self.build_context.workspace_cache(),
|
||||
credentials_cache,
|
||||
)
|
||||
@@ -1533,6 +1545,9 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
|
||||
None,
|
||||
self.build_context.locations(),
|
||||
self.build_context.sources().clone(),
|
||||
self.build_context
|
||||
.source_tree_editable_policy()
|
||||
.workspace_member_editable(None),
|
||||
self.build_context.workspace_cache(),
|
||||
credentials_cache,
|
||||
)
|
||||
@@ -1855,6 +1870,9 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
|
||||
Some(&git_member),
|
||||
self.build_context.locations(),
|
||||
self.build_context.sources().clone(),
|
||||
self.build_context
|
||||
.source_tree_editable_policy()
|
||||
.workspace_member_editable(None),
|
||||
self.build_context.workspace_cache(),
|
||||
credentials_cache,
|
||||
)
|
||||
@@ -1889,6 +1907,9 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
|
||||
Some(&git_member),
|
||||
self.build_context.locations(),
|
||||
self.build_context.sources().clone(),
|
||||
self.build_context
|
||||
.source_tree_editable_policy()
|
||||
.workspace_member_editable(None),
|
||||
self.build_context.workspace_cache(),
|
||||
credentials_cache,
|
||||
)
|
||||
@@ -1942,6 +1963,9 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
|
||||
Some(&git_member),
|
||||
self.build_context.locations(),
|
||||
self.build_context.sources().clone(),
|
||||
self.build_context
|
||||
.source_tree_editable_policy()
|
||||
.workspace_member_editable(None),
|
||||
self.build_context.workspace_cache(),
|
||||
credentials_cache,
|
||||
)
|
||||
@@ -2004,6 +2028,9 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
|
||||
Some(&git_member),
|
||||
self.build_context.locations(),
|
||||
self.build_context.sources().clone(),
|
||||
self.build_context
|
||||
.source_tree_editable_policy()
|
||||
.workspace_member_editable(None),
|
||||
self.build_context.workspace_cache(),
|
||||
credentials_cache,
|
||||
)
|
||||
|
||||
@@ -16,6 +16,9 @@ use uv_settings::{ToolOptions, ToolOptionsWire};
|
||||
#[serde(try_from = "ToolWire", into = "ToolWire")]
|
||||
pub struct Tool {
|
||||
/// The requirements requested by the user during installation.
|
||||
///
|
||||
/// The first requirement is the tool target itself; any remaining requirements come from
|
||||
/// `--with`.
|
||||
requirements: Vec<Requirement>,
|
||||
/// The constraints requested by the user during installation.
|
||||
constraints: Vec<Requirement>,
|
||||
@@ -356,6 +359,11 @@ impl Tool {
|
||||
&self.requirements
|
||||
}
|
||||
|
||||
/// Return the primary requirement for the tool itself.
|
||||
pub fn target_requirement(&self) -> Option<&Requirement> {
|
||||
self.requirements.first()
|
||||
}
|
||||
|
||||
pub fn constraints(&self) -> &[Requirement] {
|
||||
&self.constraints
|
||||
}
|
||||
|
||||
@@ -21,6 +21,36 @@ use uv_workspace::WorkspaceCache;
|
||||
|
||||
use crate::{BuildArena, BuildIsolation};
|
||||
|
||||
/// Controls how source tree requirements influence workspace-member editability during lowering.
|
||||
#[derive(Debug, Clone, Copy, Default, Eq, PartialEq)]
|
||||
pub enum SourceTreeEditablePolicy {
|
||||
/// Use project-style semantics when lowering workspace members.
|
||||
///
|
||||
/// Explicit source-tree editable settings are ignored, preserving the existing implicit
|
||||
/// editable default for workspace members.
|
||||
#[default]
|
||||
Project,
|
||||
|
||||
/// Use tool-style semantics when lowering workspace members.
|
||||
///
|
||||
/// Explicit source-tree editable settings are preserved, while implicit workspace members
|
||||
/// default to non-editable.
|
||||
Tool,
|
||||
}
|
||||
|
||||
impl SourceTreeEditablePolicy {
|
||||
/// Return the default editable mode for workspace members lowered under this policy.
|
||||
///
|
||||
/// `explicit` is the explicit editable choice on the source tree being lowered, if any. In
|
||||
/// `Tool` mode it propagates to workspace siblings; in `Project` mode it is ignored.
|
||||
pub fn workspace_member_editable(self, explicit: Option<bool>) -> bool {
|
||||
match self {
|
||||
Self::Project => true,
|
||||
Self::Tool => explicit.unwrap_or(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Avoids cyclic crate dependencies between resolver, installer and builder.
|
||||
///
|
||||
/// To resolve the dependencies of a packages, we may need to build one or more source
|
||||
@@ -99,6 +129,11 @@ pub trait BuildContext {
|
||||
/// Whether to incorporate `tool.uv.sources` when resolving requirements.
|
||||
fn sources(&self) -> &NoSources;
|
||||
|
||||
/// How source tree requirements should influence workspace-member editability.
|
||||
fn source_tree_editable_policy(&self) -> SourceTreeEditablePolicy {
|
||||
SourceTreeEditablePolicy::Project
|
||||
}
|
||||
|
||||
/// The index locations being searched.
|
||||
fn locations(&self) -> &IndexLocations;
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ use uv_python::{
|
||||
use uv_requirements::RequirementsSource;
|
||||
use uv_resolver::{ExcludeNewer, FlatIndex};
|
||||
use uv_settings::PythonInstallMirrors;
|
||||
use uv_types::{AnyErrorBuild, BuildContext, BuildStack, HashStrategy};
|
||||
use uv_types::{AnyErrorBuild, BuildContext, BuildStack, HashStrategy, SourceTreeEditablePolicy};
|
||||
use uv_workspace::pyproject::ExtraBuildDependencies;
|
||||
use uv_workspace::{DiscoveryOptions, Workspace, WorkspaceCache, WorkspaceError};
|
||||
|
||||
@@ -642,6 +642,7 @@ async fn build_package(
|
||||
&hasher,
|
||||
exclude_newer,
|
||||
sources.clone(),
|
||||
SourceTreeEditablePolicy::Project,
|
||||
workspace_cache.clone(),
|
||||
concurrency.clone(),
|
||||
preview,
|
||||
|
||||
@@ -48,7 +48,7 @@ use uv_resolver::{
|
||||
use uv_settings::PythonInstallMirrors;
|
||||
use uv_static::EnvVars;
|
||||
use uv_torch::{TorchMode, TorchSource, TorchStrategy};
|
||||
use uv_types::{EmptyInstalledPackages, HashStrategy};
|
||||
use uv_types::{EmptyInstalledPackages, HashStrategy, SourceTreeEditablePolicy};
|
||||
use uv_warnings::warn_user;
|
||||
use uv_workspace::WorkspaceCache;
|
||||
use uv_workspace::pyproject::ExtraBuildDependencies;
|
||||
@@ -546,6 +546,7 @@ pub(crate) async fn pip_compile(
|
||||
&build_hashes,
|
||||
exclude_newer.clone(),
|
||||
sources,
|
||||
SourceTreeEditablePolicy::Project,
|
||||
workspace_cache,
|
||||
concurrency.clone(),
|
||||
preview,
|
||||
|
||||
@@ -36,7 +36,7 @@ use uv_resolver::{
|
||||
};
|
||||
use uv_settings::PythonInstallMirrors;
|
||||
use uv_torch::{TorchMode, TorchSource, TorchStrategy};
|
||||
use uv_types::HashStrategy;
|
||||
use uv_types::{HashStrategy, SourceTreeEditablePolicy};
|
||||
use uv_warnings::warn_user;
|
||||
use uv_workspace::WorkspaceCache;
|
||||
use uv_workspace::pyproject::ExtraBuildDependencies;
|
||||
@@ -486,6 +486,7 @@ pub(crate) async fn pip_install(
|
||||
&build_hasher,
|
||||
exclude_newer.clone(),
|
||||
sources.clone(),
|
||||
SourceTreeEditablePolicy::Project,
|
||||
workspace_cache.clone(),
|
||||
concurrency.clone(),
|
||||
preview,
|
||||
@@ -642,6 +643,7 @@ pub(crate) async fn pip_install(
|
||||
&build_hasher,
|
||||
exclude_newer.clone(),
|
||||
sources,
|
||||
SourceTreeEditablePolicy::Project,
|
||||
workspace_cache,
|
||||
concurrency.clone(),
|
||||
preview,
|
||||
|
||||
@@ -35,7 +35,7 @@ use uv_resolver::{
|
||||
};
|
||||
use uv_settings::PythonInstallMirrors;
|
||||
use uv_torch::{TorchMode, TorchSource, TorchStrategy};
|
||||
use uv_types::HashStrategy;
|
||||
use uv_types::{HashStrategy, SourceTreeEditablePolicy};
|
||||
use uv_warnings::warn_user;
|
||||
use uv_workspace::WorkspaceCache;
|
||||
use uv_workspace::pyproject::ExtraBuildDependencies;
|
||||
@@ -393,6 +393,7 @@ pub(crate) async fn pip_sync(
|
||||
&build_hasher,
|
||||
exclude_newer.clone(),
|
||||
sources.clone(),
|
||||
SourceTreeEditablePolicy::Project,
|
||||
workspace_cache.clone(),
|
||||
concurrency.clone(),
|
||||
preview,
|
||||
@@ -533,6 +534,7 @@ pub(crate) async fn pip_sync(
|
||||
&build_hasher,
|
||||
exclude_newer.clone(),
|
||||
sources,
|
||||
SourceTreeEditablePolicy::Project,
|
||||
workspace_cache,
|
||||
concurrency.clone(),
|
||||
preview,
|
||||
|
||||
@@ -36,7 +36,7 @@ use uv_requirements::{NamedRequirementsResolver, RequirementsSource, Requirement
|
||||
use uv_resolver::FlatIndex;
|
||||
use uv_scripts::{Pep723Metadata, Pep723Script};
|
||||
use uv_settings::PythonInstallMirrors;
|
||||
use uv_types::{BuildIsolation, HashStrategy};
|
||||
use uv_types::{BuildIsolation, HashStrategy, SourceTreeEditablePolicy};
|
||||
use uv_warnings::warn_user_once;
|
||||
use uv_workspace::pyproject::{DependencyType, Source, SourceError, Sources, ToolUvSources};
|
||||
use uv_workspace::pyproject_mut::{AddBoundsKind, ArrayEdit, DependencyTarget, PyProjectTomlMut};
|
||||
@@ -454,6 +454,7 @@ pub(crate) async fn add(
|
||||
&build_hasher,
|
||||
settings.resolver.exclude_newer.clone(),
|
||||
sources,
|
||||
SourceTreeEditablePolicy::Project,
|
||||
// No workspace caching since `uv add` changes the workspace definition.
|
||||
WorkspaceCache::default(),
|
||||
concurrency.clone(),
|
||||
|
||||
@@ -21,6 +21,7 @@ use uv_distribution_types::{
|
||||
use uv_fs::PythonExt;
|
||||
use uv_preview::Preview;
|
||||
use uv_python::{Interpreter, PythonEnvironment, canonicalize_executable};
|
||||
use uv_types::SourceTreeEditablePolicy;
|
||||
use uv_workspace::WorkspaceCache;
|
||||
|
||||
/// An ephemeral [`PythonEnvironment`] for running an individual command.
|
||||
@@ -141,6 +142,7 @@ impl CachedEnvironment {
|
||||
spec,
|
||||
&interpreter,
|
||||
python_platform,
|
||||
SourceTreeEditablePolicy::Project,
|
||||
build_constraints.clone(),
|
||||
&settings.resolver,
|
||||
client_builder,
|
||||
|
||||
@@ -36,7 +36,9 @@ use uv_resolver::{
|
||||
};
|
||||
use uv_scripts::Pep723Script;
|
||||
use uv_settings::PythonInstallMirrors;
|
||||
use uv_types::{BuildContext, BuildIsolation, EmptyInstalledPackages, HashStrategy};
|
||||
use uv_types::{
|
||||
BuildContext, BuildIsolation, EmptyInstalledPackages, HashStrategy, SourceTreeEditablePolicy,
|
||||
};
|
||||
use uv_warnings::{warn_user, warn_user_once};
|
||||
use uv_workspace::{DiscoveryOptions, Editability, Workspace, WorkspaceCache, WorkspaceMember};
|
||||
|
||||
@@ -795,6 +797,7 @@ async fn do_lock(
|
||||
&build_hasher,
|
||||
exclude_newer.clone(),
|
||||
sources.clone(),
|
||||
SourceTreeEditablePolicy::Project,
|
||||
workspace_cache.clone(),
|
||||
concurrency.clone(),
|
||||
preview,
|
||||
|
||||
@@ -44,7 +44,7 @@ use uv_scripts::Pep723ItemRef;
|
||||
use uv_settings::PythonInstallMirrors;
|
||||
use uv_static::EnvVars;
|
||||
use uv_torch::{TorchSource, TorchStrategy};
|
||||
use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy};
|
||||
use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy, SourceTreeEditablePolicy};
|
||||
use uv_virtualenv::remove_virtualenv;
|
||||
use uv_warnings::{warn_user, warn_user_once};
|
||||
use uv_workspace::dependency_groups::DependencyGroupError;
|
||||
@@ -1928,6 +1928,7 @@ pub(crate) async fn resolve_names(
|
||||
&build_hasher,
|
||||
exclude_newer.clone(),
|
||||
sources.clone(),
|
||||
SourceTreeEditablePolicy::Project,
|
||||
workspace_cache.clone(),
|
||||
concurrency.clone(),
|
||||
preview,
|
||||
@@ -1996,6 +1997,7 @@ pub(crate) async fn resolve_environment(
|
||||
spec: EnvironmentSpecification<'_>,
|
||||
interpreter: &Interpreter,
|
||||
python_platform: Option<&TargetTriple>,
|
||||
source_tree_editable_policy: SourceTreeEditablePolicy,
|
||||
build_constraints: Constraints,
|
||||
settings: &ResolverSettings,
|
||||
client_builder: &BaseClientBuilder<'_>,
|
||||
@@ -2167,6 +2169,7 @@ pub(crate) async fn resolve_environment(
|
||||
&build_hasher,
|
||||
exclude_newer.clone(),
|
||||
sources.clone(),
|
||||
source_tree_editable_policy,
|
||||
workspace_cache.clone(),
|
||||
concurrency.clone(),
|
||||
preview,
|
||||
@@ -2307,6 +2310,7 @@ pub(crate) async fn sync_environment(
|
||||
&build_hasher,
|
||||
exclude_newer.clone(),
|
||||
sources,
|
||||
SourceTreeEditablePolicy::Project,
|
||||
workspace_cache,
|
||||
concurrency.clone(),
|
||||
preview,
|
||||
@@ -2366,6 +2370,7 @@ pub(crate) async fn update_environment(
|
||||
spec: RequirementsSpecification,
|
||||
modifications: Modifications,
|
||||
python_platform: Option<&TargetTriple>,
|
||||
source_tree_editable_policy: SourceTreeEditablePolicy,
|
||||
build_constraints: Constraints,
|
||||
extra_build_requires: ExtraBuildRequires,
|
||||
settings: &ResolverInstallerSettings,
|
||||
@@ -2563,6 +2568,7 @@ pub(crate) async fn update_environment(
|
||||
&build_hasher,
|
||||
exclude_newer.clone(),
|
||||
sources.clone(),
|
||||
source_tree_editable_policy,
|
||||
workspace_cache.clone(),
|
||||
concurrency.clone(),
|
||||
preview,
|
||||
@@ -2603,7 +2609,6 @@ pub(crate) async fn update_environment(
|
||||
Ok((resolution, hasher)) => (Resolution::from(resolution), hasher),
|
||||
Err(err) => return Err(err.into()),
|
||||
};
|
||||
|
||||
// Sync the environment.
|
||||
let changelog = pip::operations::install(
|
||||
&resolution,
|
||||
|
||||
@@ -41,6 +41,7 @@ use uv_scripts::{Pep723Error, Pep723Item, Pep723Metadata, Pep723Script};
|
||||
use uv_settings::{EnvironmentOptions, FilesystemOptions, PythonInstallMirrors};
|
||||
use uv_shell::runnable::WindowsRunnable;
|
||||
use uv_static::EnvVars;
|
||||
use uv_types::SourceTreeEditablePolicy;
|
||||
use uv_warnings::warn_user;
|
||||
use uv_workspace::{DiscoveryOptions, VirtualProject, WorkspaceCache, WorkspaceError};
|
||||
|
||||
@@ -456,6 +457,7 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl
|
||||
spec,
|
||||
modifications,
|
||||
python_platform.as_ref(),
|
||||
SourceTreeEditablePolicy::Project,
|
||||
build_constraints.unwrap_or_default(),
|
||||
script_extra_build_requires,
|
||||
&settings,
|
||||
|
||||
@@ -32,7 +32,7 @@ use uv_python::{PythonDownloads, PythonEnvironment, PythonPreference, PythonRequ
|
||||
use uv_resolver::{FlatIndex, ForkStrategy, Installable, Lock, PrereleaseMode, ResolutionMode};
|
||||
use uv_scripts::Pep723Script;
|
||||
use uv_settings::PythonInstallMirrors;
|
||||
use uv_types::{BuildIsolation, HashStrategy};
|
||||
use uv_types::{BuildIsolation, HashStrategy, SourceTreeEditablePolicy};
|
||||
use uv_warnings::warn_user;
|
||||
use uv_workspace::pyproject::Source;
|
||||
use uv_workspace::{DiscoveryOptions, MemberDiscovery, VirtualProject, Workspace, WorkspaceCache};
|
||||
@@ -267,6 +267,7 @@ pub(crate) async fn sync(
|
||||
spec,
|
||||
modifications,
|
||||
python_platform.as_ref(),
|
||||
SourceTreeEditablePolicy::Project,
|
||||
build_constraints.unwrap_or_default(),
|
||||
script_extra_build_requires,
|
||||
&settings,
|
||||
@@ -844,6 +845,7 @@ pub(super) async fn do_sync(
|
||||
&build_hasher,
|
||||
exclude_newer.clone(),
|
||||
sources.clone(),
|
||||
SourceTreeEditablePolicy::Project,
|
||||
workspace_cache.clone(),
|
||||
concurrency.clone(),
|
||||
preview,
|
||||
@@ -880,25 +882,6 @@ pub(super) async fn do_sync(
|
||||
Ok(changelog)
|
||||
}
|
||||
|
||||
/// Filter out any virtual workspace members.
|
||||
fn apply_no_virtual_project(resolution: Resolution) -> Resolution {
|
||||
resolution.filter(|dist| {
|
||||
let ResolvedDist::Installable { dist, .. } = dist else {
|
||||
return true;
|
||||
};
|
||||
|
||||
let Dist::Source(dist) = dist.as_ref() else {
|
||||
return true;
|
||||
};
|
||||
|
||||
let SourceDist::Directory(dist) = dist else {
|
||||
return true;
|
||||
};
|
||||
|
||||
!dist.r#virtual.unwrap_or(false)
|
||||
})
|
||||
}
|
||||
|
||||
/// If necessary, convert any editable requirements to non-editable.
|
||||
fn apply_editable_mode(resolution: Resolution, editable: Option<EditableMode>) -> Resolution {
|
||||
match editable {
|
||||
@@ -933,7 +916,7 @@ fn apply_editable_mode(resolution: Resolution, editable: Option<EditableMode>) -
|
||||
})
|
||||
}),
|
||||
|
||||
// Filter out any editable distributions.
|
||||
// If a package is editable, map it to a non-editable distribution.
|
||||
Some(EditableMode::NonEditable) => resolution.map(|dist| {
|
||||
let ResolvedDist::Installable { dist, version } = dist else {
|
||||
return None;
|
||||
@@ -963,6 +946,25 @@ fn apply_editable_mode(resolution: Resolution, editable: Option<EditableMode>) -
|
||||
}
|
||||
}
|
||||
|
||||
/// Filter out any virtual workspace members.
|
||||
fn apply_no_virtual_project(resolution: Resolution) -> Resolution {
|
||||
resolution.filter(|dist| {
|
||||
let ResolvedDist::Installable { dist, .. } = dist else {
|
||||
return true;
|
||||
};
|
||||
|
||||
let Dist::Source(dist) = dist.as_ref() else {
|
||||
return true;
|
||||
};
|
||||
|
||||
let SourceDist::Directory(dist) = dist else {
|
||||
return true;
|
||||
};
|
||||
|
||||
!dist.r#virtual.unwrap_or(false)
|
||||
})
|
||||
}
|
||||
|
||||
/// Extract any credentials that are defined on the workspace dependencies themselves. While we
|
||||
/// don't store plaintext credentials in the `uv.lock`, we do respect credentials that are defined
|
||||
/// in the `pyproject.toml`.
|
||||
|
||||
@@ -10,8 +10,7 @@ use std::{
|
||||
use tracing::{debug, warn};
|
||||
use uv_cache::Cache;
|
||||
use uv_client::BaseClientBuilder;
|
||||
use uv_distribution_types::Requirement;
|
||||
use uv_distribution_types::{InstalledDist, Name};
|
||||
use uv_distribution_types::{InstalledDist, Name, Requirement};
|
||||
use uv_fs::Simplified;
|
||||
#[cfg(unix)]
|
||||
use uv_fs::replace_symlink;
|
||||
|
||||
@@ -29,6 +29,7 @@ use uv_python::{
|
||||
use uv_requirements::{RequirementsSource, RequirementsSpecification};
|
||||
use uv_settings::{PythonInstallMirrors, ResolverInstallerOptions, ToolOptions};
|
||||
use uv_tool::InstalledTools;
|
||||
use uv_types::SourceTreeEditablePolicy;
|
||||
use uv_warnings::{warn_user, warn_user_once};
|
||||
use uv_workspace::WorkspaceCache;
|
||||
|
||||
@@ -510,6 +511,8 @@ pub(crate) async fn install(
|
||||
|
||||
// Check if the installed packages meet the requirements.
|
||||
let site_packages = SitePackages::from_environment(environment.environment())?;
|
||||
// TODO(charlie): This fast path only validates the explicit requested
|
||||
// requirements. It can miss editable-mode drift for implicit workspace members.
|
||||
if matches!(
|
||||
site_packages.satisfies_requirements(
|
||||
requirements.iter(),
|
||||
@@ -576,6 +579,7 @@ pub(crate) async fn install(
|
||||
spec,
|
||||
Modifications::Exact,
|
||||
python_platform.as_ref(),
|
||||
SourceTreeEditablePolicy::Tool,
|
||||
Constraints::from_requirements(build_constraints.iter().cloned()),
|
||||
ExtraBuildRequires::default(),
|
||||
&settings,
|
||||
@@ -620,6 +624,7 @@ pub(crate) async fn install(
|
||||
spec.clone(),
|
||||
&interpreter,
|
||||
python_platform.as_ref(),
|
||||
SourceTreeEditablePolicy::Tool,
|
||||
Constraints::from_requirements(build_constraints.iter().cloned()),
|
||||
&settings.resolver,
|
||||
&client_builder,
|
||||
@@ -676,6 +681,7 @@ pub(crate) async fn install(
|
||||
spec,
|
||||
&interpreter,
|
||||
python_platform.as_ref(),
|
||||
SourceTreeEditablePolicy::Tool,
|
||||
Constraints::from_requirements(build_constraints.iter().cloned()),
|
||||
&settings.resolver,
|
||||
&client_builder,
|
||||
|
||||
@@ -21,6 +21,7 @@ use uv_python::{
|
||||
use uv_requirements::RequirementsSpecification;
|
||||
use uv_settings::{Combine, PythonInstallMirrors, ResolverInstallerOptions, ToolOptions};
|
||||
use uv_tool::{InstalledTools, Tool};
|
||||
use uv_types::SourceTreeEditablePolicy;
|
||||
use uv_warnings::write_error_chain;
|
||||
use uv_workspace::WorkspaceCache;
|
||||
|
||||
@@ -338,7 +339,6 @@ async fn upgrade_tool(
|
||||
existing_tool_receipt.overrides().to_vec(),
|
||||
existing_tool_receipt.excludes().to_vec(),
|
||||
);
|
||||
|
||||
// Initialize any shared state.
|
||||
let state = PlatformState::default();
|
||||
|
||||
@@ -352,6 +352,7 @@ async fn upgrade_tool(
|
||||
spec.into(),
|
||||
interpreter,
|
||||
python_platform,
|
||||
SourceTreeEditablePolicy::Tool,
|
||||
build_constraints.clone(),
|
||||
&settings.resolver,
|
||||
client_builder,
|
||||
@@ -397,6 +398,7 @@ async fn upgrade_tool(
|
||||
spec,
|
||||
Modifications::Exact,
|
||||
python_platform,
|
||||
SourceTreeEditablePolicy::Tool,
|
||||
build_constraints,
|
||||
ExtraBuildRequires::default(),
|
||||
&settings,
|
||||
|
||||
@@ -28,7 +28,9 @@ use uv_python::{
|
||||
use uv_resolver::{ExcludeNewer, FlatIndex};
|
||||
use uv_settings::PythonInstallMirrors;
|
||||
use uv_shell::{Shell, shlex_posix, shlex_windows};
|
||||
use uv_types::{AnyErrorBuild, BuildContext, BuildIsolation, BuildStack, HashStrategy};
|
||||
use uv_types::{
|
||||
AnyErrorBuild, BuildContext, BuildIsolation, BuildStack, HashStrategy, SourceTreeEditablePolicy,
|
||||
};
|
||||
use uv_virtualenv::OnExisting;
|
||||
use uv_warnings::warn_user;
|
||||
use uv_workspace::{DiscoveryOptions, VirtualProject, WorkspaceCache, WorkspaceError};
|
||||
@@ -272,6 +274,7 @@ pub(crate) async fn venv(
|
||||
&build_hasher,
|
||||
exclude_newer,
|
||||
sources,
|
||||
SourceTreeEditablePolicy::Project,
|
||||
workspace_cache.clone(),
|
||||
concurrency,
|
||||
preview,
|
||||
|
||||
@@ -6,7 +6,7 @@ use anyhow::Result;
|
||||
use assert_cmd::assert::OutputAssertExt;
|
||||
use assert_fs::{
|
||||
assert::PathAssert,
|
||||
fixture::{FileTouch, FileWriteStr, PathChild},
|
||||
fixture::{FileTouch, FileWriteStr, PathChild, PathCreateDir},
|
||||
};
|
||||
use indoc::indoc;
|
||||
use insta::assert_snapshot;
|
||||
@@ -579,6 +579,808 @@ fn tool_install_with_editable() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_install_workspace_members_do_not_override_explicit_with_requirements() -> Result<()> {
|
||||
let context = uv_test::test_context!("3.12").with_filtered_exe_suffix();
|
||||
let with_editable_tool_dir = context.temp_dir.child("tools-with-editable");
|
||||
let with_editable_bin_dir = context.temp_dir.child("bin-with-editable");
|
||||
let with_tool_dir = context.temp_dir.child("tools-with");
|
||||
let with_bin_dir = context.temp_dir.child("bin-with");
|
||||
|
||||
let root_pyproject = context.temp_dir.child("pyproject.toml");
|
||||
root_pyproject.write_str(indoc! {
|
||||
r#"
|
||||
[project]
|
||||
name = "root"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[project.scripts]
|
||||
root_cli = "root:main"
|
||||
|
||||
[build-system]
|
||||
requires = ["uv_build>=0.7,<10000"]
|
||||
build-backend = "uv_build"
|
||||
|
||||
[tool.uv.workspace]
|
||||
members = ["child"]
|
||||
"#
|
||||
})?;
|
||||
|
||||
let root_src = context.temp_dir.child("src").child("root");
|
||||
root_src.create_dir_all()?;
|
||||
root_src.child("__init__.py").write_str(indoc! {
|
||||
r"
|
||||
def main():
|
||||
import child
|
||||
print(child.MESSAGE)
|
||||
"
|
||||
})?;
|
||||
|
||||
let child = context.temp_dir.child("child");
|
||||
child.create_dir_all()?;
|
||||
child.child("pyproject.toml").write_str(indoc! {r#"
|
||||
[project]
|
||||
name = "child"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[build-system]
|
||||
requires = ["uv_build>=0.7,<10000"]
|
||||
build-backend = "uv_build"
|
||||
"#})?;
|
||||
|
||||
let child_src = child.child("src").child("child");
|
||||
child_src.create_dir_all()?;
|
||||
child_src
|
||||
.child("__init__.py")
|
||||
.write_str("MESSAGE = 'OK'\n")?;
|
||||
|
||||
let status = context
|
||||
.tool_install()
|
||||
.arg("--with-editable")
|
||||
.arg(child.path())
|
||||
.arg(context.temp_dir.path())
|
||||
.env(EnvVars::UV_TOOL_DIR, with_editable_tool_dir.as_os_str())
|
||||
.env(EnvVars::XDG_BIN_HOME, with_editable_bin_dir.as_os_str())
|
||||
.env(EnvVars::PATH, with_editable_bin_dir.as_os_str())
|
||||
.status()
|
||||
.expect("failed to run uv tool install with --with-editable");
|
||||
assert!(status.success());
|
||||
|
||||
uv_snapshot!(context.filters(), Command::new("root_cli").env(EnvVars::PATH, with_editable_bin_dir.as_os_str()), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
OK
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
child_src
|
||||
.child("__init__.py")
|
||||
.write_str("MESSAGE = 'CHANGED'\n")?;
|
||||
|
||||
uv_snapshot!(context.filters(), Command::new("root_cli").env(EnvVars::PATH, with_editable_bin_dir.as_os_str()), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
CHANGED
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
child_src
|
||||
.child("__init__.py")
|
||||
.write_str("MESSAGE = 'OK'\n")?;
|
||||
|
||||
let status = context
|
||||
.tool_install()
|
||||
.arg("--editable")
|
||||
.arg("--with")
|
||||
.arg(child.path())
|
||||
.arg(context.temp_dir.path())
|
||||
.env(EnvVars::UV_TOOL_DIR, with_tool_dir.as_os_str())
|
||||
.env(EnvVars::XDG_BIN_HOME, with_bin_dir.as_os_str())
|
||||
.env(EnvVars::PATH, with_bin_dir.as_os_str())
|
||||
.status()
|
||||
.expect("failed to run uv tool install with --with");
|
||||
assert!(status.success());
|
||||
|
||||
uv_snapshot!(context.filters(), Command::new("root_cli").env(EnvVars::PATH, with_bin_dir.as_os_str()), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
OK
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
child_src
|
||||
.child("__init__.py")
|
||||
.write_str("MESSAGE = 'CHANGED'\n")?;
|
||||
|
||||
uv_snapshot!(context.filters(), Command::new("root_cli").env(EnvVars::PATH, with_bin_dir.as_os_str()), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
OK
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_install_preserves_mixed_workspace_member_editability() -> Result<()> {
|
||||
let context = uv_test::test_context!("3.12").with_filtered_exe_suffix();
|
||||
let tool_dir = context.temp_dir.child("tools");
|
||||
let bin_dir = context.temp_dir.child("bin");
|
||||
|
||||
let tool_root = context.temp_dir.child("tool-root");
|
||||
tool_root.create_dir_all()?;
|
||||
tool_root.child("pyproject.toml").write_str(indoc! {r#"
|
||||
[project]
|
||||
name = "tool-root"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[project.scripts]
|
||||
root_cli = "tool_root:main"
|
||||
|
||||
[build-system]
|
||||
requires = ["uv_build>=0.7,<10000"]
|
||||
build-backend = "uv_build"
|
||||
"#})?;
|
||||
let tool_root_src = tool_root.child("src").child("tool_root");
|
||||
tool_root_src.create_dir_all()?;
|
||||
tool_root_src.child("__init__.py").write_str(indoc! {
|
||||
r#"
|
||||
def main():
|
||||
import importlib.metadata
|
||||
import other_child
|
||||
|
||||
print(f"{importlib.metadata.version('tool-root')} {other_child.MESSAGE}")
|
||||
"#
|
||||
})?;
|
||||
|
||||
let other_workspace = context.temp_dir.child("other-workspace");
|
||||
other_workspace.create_dir_all()?;
|
||||
other_workspace
|
||||
.child("pyproject.toml")
|
||||
.write_str(indoc! {r#"
|
||||
[project]
|
||||
name = "other-workspace"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = ["other-child"]
|
||||
|
||||
[build-system]
|
||||
requires = ["uv_build>=0.7,<10000"]
|
||||
build-backend = "uv_build"
|
||||
|
||||
[tool.uv.sources]
|
||||
other-child = { workspace = true }
|
||||
|
||||
[tool.uv.workspace]
|
||||
members = ["packages/*"]
|
||||
"#})?;
|
||||
let other_workspace_src = other_workspace.child("src").child("other_workspace");
|
||||
other_workspace_src.create_dir_all()?;
|
||||
other_workspace_src.child("__init__.py").touch()?;
|
||||
|
||||
let other_child = other_workspace.child("packages").child("other-child");
|
||||
other_child.create_dir_all()?;
|
||||
other_child.child("pyproject.toml").write_str(indoc! {r#"
|
||||
[project]
|
||||
name = "other-child"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[build-system]
|
||||
requires = ["uv_build>=0.7,<10000"]
|
||||
build-backend = "uv_build"
|
||||
"#})?;
|
||||
let other_child_src = other_child.child("src").child("other_child");
|
||||
other_child_src.create_dir_all()?;
|
||||
other_child_src
|
||||
.child("__init__.py")
|
||||
.write_str("MESSAGE = 'OK'\n")?;
|
||||
|
||||
let status = context
|
||||
.tool_install()
|
||||
.arg("--with-editable")
|
||||
.arg(other_workspace.path())
|
||||
.arg(tool_root.path())
|
||||
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
|
||||
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
|
||||
.env(EnvVars::PATH, bin_dir.as_os_str())
|
||||
.status()
|
||||
.expect("failed to run uv tool install with mixed workspace editability");
|
||||
assert!(status.success());
|
||||
|
||||
uv_snapshot!(context.filters(), Command::new("root_cli").env(EnvVars::PATH, bin_dir.as_os_str()), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
0.1.0 OK
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
other_child_src
|
||||
.child("__init__.py")
|
||||
.write_str("MESSAGE = 'CHANGED'\n")?;
|
||||
|
||||
uv_snapshot!(context.filters(), Command::new("root_cli").env(EnvVars::PATH, bin_dir.as_os_str()), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
0.1.0 CHANGED
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_install_preserves_mixed_workspace_member_non_editability() -> Result<()> {
|
||||
let context = uv_test::test_context!("3.12").with_filtered_exe_suffix();
|
||||
let tool_dir = context.temp_dir.child("tools");
|
||||
let bin_dir = context.temp_dir.child("bin");
|
||||
|
||||
let tool_root = context.temp_dir.child("tool-root");
|
||||
tool_root.create_dir_all()?;
|
||||
tool_root.child("pyproject.toml").write_str(indoc! {r#"
|
||||
[project]
|
||||
name = "tool-root"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[project.scripts]
|
||||
root_cli = "tool_root:main"
|
||||
|
||||
[build-system]
|
||||
requires = ["uv_build>=0.7,<10000"]
|
||||
build-backend = "uv_build"
|
||||
"#})?;
|
||||
let tool_root_src = tool_root.child("src").child("tool_root");
|
||||
tool_root_src.create_dir_all()?;
|
||||
tool_root_src.child("__init__.py").write_str(indoc! {
|
||||
r#"
|
||||
def main():
|
||||
import importlib.metadata
|
||||
import other_child
|
||||
|
||||
print(f"{importlib.metadata.version('tool-root')} {other_child.MESSAGE}")
|
||||
"#
|
||||
})?;
|
||||
|
||||
let other_workspace = context.temp_dir.child("other-workspace");
|
||||
other_workspace.create_dir_all()?;
|
||||
other_workspace
|
||||
.child("pyproject.toml")
|
||||
.write_str(indoc! {r#"
|
||||
[project]
|
||||
name = "other-workspace"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = ["other-child"]
|
||||
|
||||
[build-system]
|
||||
requires = ["uv_build>=0.7,<10000"]
|
||||
build-backend = "uv_build"
|
||||
|
||||
[tool.uv.sources]
|
||||
other-child = { workspace = true }
|
||||
|
||||
[tool.uv.workspace]
|
||||
members = ["packages/*"]
|
||||
"#})?;
|
||||
let other_workspace_src = other_workspace.child("src").child("other_workspace");
|
||||
other_workspace_src.create_dir_all()?;
|
||||
other_workspace_src.child("__init__.py").touch()?;
|
||||
|
||||
let other_child = other_workspace.child("packages").child("other-child");
|
||||
other_child.create_dir_all()?;
|
||||
other_child.child("pyproject.toml").write_str(indoc! {r#"
|
||||
[project]
|
||||
name = "other-child"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[build-system]
|
||||
requires = ["uv_build>=0.7,<10000"]
|
||||
build-backend = "uv_build"
|
||||
"#})?;
|
||||
let other_child_src = other_child.child("src").child("other_child");
|
||||
other_child_src.create_dir_all()?;
|
||||
other_child_src
|
||||
.child("__init__.py")
|
||||
.write_str("MESSAGE = 'OK'\n")?;
|
||||
|
||||
let status = context
|
||||
.tool_install()
|
||||
.arg("--editable")
|
||||
.arg("--with")
|
||||
.arg(other_workspace.path())
|
||||
.arg(tool_root.path())
|
||||
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
|
||||
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
|
||||
.env(EnvVars::PATH, bin_dir.as_os_str())
|
||||
.status()
|
||||
.expect("failed to run uv tool install with mixed workspace editability");
|
||||
assert!(status.success());
|
||||
|
||||
uv_snapshot!(context.filters(), Command::new("root_cli").env(EnvVars::PATH, bin_dir.as_os_str()), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
0.1.0 OK
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
other_child_src
|
||||
.child("__init__.py")
|
||||
.write_str("MESSAGE = 'CHANGED'\n")?;
|
||||
|
||||
uv_snapshot!(context.filters(), Command::new("root_cli").env(EnvVars::PATH, bin_dir.as_os_str()), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
0.1.0 OK
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_install_reinstall_converts_workspace_members_to_non_editable() -> Result<()> {
|
||||
let context = uv_test::test_context!("3.12").with_filtered_exe_suffix();
|
||||
let tool_dir = context.temp_dir.child("tools");
|
||||
let bin_dir = context.temp_dir.child("bin");
|
||||
|
||||
let root_pyproject = context.temp_dir.child("pyproject.toml");
|
||||
root_pyproject.write_str(indoc! {
|
||||
r#"
|
||||
[project]
|
||||
name = "root"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = ["child"]
|
||||
|
||||
[project.scripts]
|
||||
root_cli = "root:main"
|
||||
|
||||
[build-system]
|
||||
requires = ["uv_build>=0.7,<10000"]
|
||||
build-backend = "uv_build"
|
||||
|
||||
[tool.uv.sources]
|
||||
child = { workspace = true }
|
||||
|
||||
[tool.uv.workspace]
|
||||
members = ["child"]
|
||||
"#
|
||||
})?;
|
||||
|
||||
let root_src = context.temp_dir.child("src").child("root");
|
||||
root_src.create_dir_all()?;
|
||||
root_src.child("__init__.py").write_str(indoc! {
|
||||
r"
|
||||
def main():
|
||||
import child
|
||||
print(child.MESSAGE)
|
||||
"
|
||||
})?;
|
||||
|
||||
let child = context.temp_dir.child("child");
|
||||
child.create_dir_all()?;
|
||||
child.child("pyproject.toml").write_str(indoc! {r#"
|
||||
[project]
|
||||
name = "child"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[build-system]
|
||||
requires = ["uv_build>=0.7,<10000"]
|
||||
build-backend = "uv_build"
|
||||
"#})?;
|
||||
|
||||
let child_src = child.child("src").child("child");
|
||||
child_src.create_dir_all()?;
|
||||
child_src
|
||||
.child("__init__.py")
|
||||
.write_str("MESSAGE = 'OK'\n")?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.tool_install()
|
||||
.arg("--editable")
|
||||
.arg(context.temp_dir.path())
|
||||
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
|
||||
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
|
||||
.env(EnvVars::PATH, bin_dir.as_os_str()), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 2 packages in [TIME]
|
||||
Prepared 2 packages in [TIME]
|
||||
Installed 2 packages in [TIME]
|
||||
+ child==0.1.0 (from file://[TEMP_DIR]/child)
|
||||
+ root==0.1.0 (from file://[TEMP_DIR]/)
|
||||
Installed 1 executable: root_cli
|
||||
");
|
||||
|
||||
uv_snapshot!(context.filters(), Command::new("root_cli").env(EnvVars::PATH, bin_dir.as_os_str()), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
OK
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
let status = context
|
||||
.tool_install()
|
||||
.arg("--reinstall")
|
||||
.arg(context.temp_dir.path())
|
||||
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
|
||||
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
|
||||
.env(EnvVars::PATH, bin_dir.as_os_str())
|
||||
.status()
|
||||
.expect("failed to run uv tool install --reinstall");
|
||||
assert!(status.success());
|
||||
|
||||
child_src
|
||||
.child("__init__.py")
|
||||
.write_str("MESSAGE = 'CHANGED'\n")?;
|
||||
|
||||
uv_snapshot!(context.filters(), Command::new("root_cli").env(EnvVars::PATH, bin_dir.as_os_str()), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
OK
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_install_workspace_members_are_non_editable_by_default() -> Result<()> {
|
||||
let context = uv_test::test_context!("3.12").with_filtered_exe_suffix();
|
||||
let tool_dir = context.temp_dir.child("tools");
|
||||
let bin_dir = context.temp_dir.child("bin");
|
||||
|
||||
let root_pyproject = context.temp_dir.child("pyproject.toml");
|
||||
root_pyproject.write_str(indoc! {
|
||||
r#"
|
||||
[project]
|
||||
name = "root"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = ["child"]
|
||||
|
||||
[project.scripts]
|
||||
root_cli = "root:main"
|
||||
|
||||
[build-system]
|
||||
requires = ["uv_build>=0.7,<10000"]
|
||||
build-backend = "uv_build"
|
||||
|
||||
[tool.uv.sources]
|
||||
child = { workspace = true }
|
||||
|
||||
[tool.uv.workspace]
|
||||
members = ["child"]
|
||||
"#
|
||||
})?;
|
||||
|
||||
let root_src = context.temp_dir.child("src").child("root");
|
||||
root_src.create_dir_all()?;
|
||||
root_src.child("__init__.py").write_str(indoc! {
|
||||
r"
|
||||
def main():
|
||||
import child
|
||||
print(child.MESSAGE)
|
||||
"
|
||||
})?;
|
||||
|
||||
let child = context.temp_dir.child("child");
|
||||
child.create_dir_all()?;
|
||||
child.child("pyproject.toml").write_str(indoc! {r#"
|
||||
[project]
|
||||
name = "child"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[build-system]
|
||||
requires = ["uv_build>=0.7,<10000"]
|
||||
build-backend = "uv_build"
|
||||
"#})?;
|
||||
|
||||
let child_src = child.child("src").child("child");
|
||||
child_src.create_dir_all()?;
|
||||
child_src
|
||||
.child("__init__.py")
|
||||
.write_str("MESSAGE = 'OK'\n")?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.tool_install()
|
||||
.arg(context.temp_dir.path())
|
||||
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
|
||||
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
|
||||
.env(EnvVars::PATH, bin_dir.as_os_str()), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 2 packages in [TIME]
|
||||
Prepared 2 packages in [TIME]
|
||||
Installed 2 packages in [TIME]
|
||||
+ child==0.1.0 (from file://[TEMP_DIR]/child)
|
||||
+ root==0.1.0 (from file://[TEMP_DIR]/)
|
||||
Installed 1 executable: root_cli
|
||||
");
|
||||
|
||||
uv_snapshot!(context.filters(), Command::new("root_cli").env(EnvVars::PATH, bin_dir.as_os_str()), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
OK
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
child_src
|
||||
.child("__init__.py")
|
||||
.write_str("MESSAGE = 'CHANGED'\n")?;
|
||||
|
||||
uv_snapshot!(context.filters(), Command::new("root_cli").env(EnvVars::PATH, bin_dir.as_os_str()), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
OK
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_install_workspace_members_honor_editable_flag() -> Result<()> {
|
||||
let context = uv_test::test_context!("3.12").with_filtered_exe_suffix();
|
||||
let tool_dir = context.temp_dir.child("tools");
|
||||
let bin_dir = context.temp_dir.child("bin");
|
||||
|
||||
let root_pyproject = context.temp_dir.child("pyproject.toml");
|
||||
root_pyproject.write_str(indoc! {
|
||||
r#"
|
||||
[project]
|
||||
name = "root"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = ["child"]
|
||||
|
||||
[project.scripts]
|
||||
root_cli = "root:main"
|
||||
|
||||
[build-system]
|
||||
requires = ["uv_build>=0.7,<10000"]
|
||||
build-backend = "uv_build"
|
||||
|
||||
[tool.uv.sources]
|
||||
child = { workspace = true }
|
||||
|
||||
[tool.uv.workspace]
|
||||
members = ["child"]
|
||||
"#
|
||||
})?;
|
||||
|
||||
let root_src = context.temp_dir.child("src").child("root");
|
||||
root_src.create_dir_all()?;
|
||||
root_src.child("__init__.py").write_str(indoc! {
|
||||
r"
|
||||
def main():
|
||||
import child
|
||||
print(child.MESSAGE)
|
||||
"
|
||||
})?;
|
||||
|
||||
let child = context.temp_dir.child("child");
|
||||
child.create_dir_all()?;
|
||||
child.child("pyproject.toml").write_str(indoc! {r#"
|
||||
[project]
|
||||
name = "child"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[build-system]
|
||||
requires = ["uv_build>=0.7,<10000"]
|
||||
build-backend = "uv_build"
|
||||
"#})?;
|
||||
|
||||
let child_src = child.child("src").child("child");
|
||||
child_src.create_dir_all()?;
|
||||
child_src
|
||||
.child("__init__.py")
|
||||
.write_str("MESSAGE = 'OK'\n")?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.tool_install()
|
||||
.arg("--editable")
|
||||
.arg(context.temp_dir.path())
|
||||
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
|
||||
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
|
||||
.env(EnvVars::PATH, bin_dir.as_os_str()), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 2 packages in [TIME]
|
||||
Prepared 2 packages in [TIME]
|
||||
Installed 2 packages in [TIME]
|
||||
+ child==0.1.0 (from file://[TEMP_DIR]/child)
|
||||
+ root==0.1.0 (from file://[TEMP_DIR]/)
|
||||
Installed 1 executable: root_cli
|
||||
");
|
||||
|
||||
uv_snapshot!(context.filters(), Command::new("root_cli").env(EnvVars::PATH, bin_dir.as_os_str()), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
OK
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
child_src
|
||||
.child("__init__.py")
|
||||
.write_str("MESSAGE = 'CHANGED'\n")?;
|
||||
|
||||
uv_snapshot!(context.filters(), Command::new("root_cli").env(EnvVars::PATH, bin_dir.as_os_str()), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
CHANGED
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_install_workspace_members_honor_source_editable_flag() -> Result<()> {
|
||||
let context = uv_test::test_context!("3.12").with_filtered_exe_suffix();
|
||||
let tool_dir = context.temp_dir.child("tools");
|
||||
let bin_dir = context.temp_dir.child("bin");
|
||||
|
||||
let root_pyproject = context.temp_dir.child("pyproject.toml");
|
||||
root_pyproject.write_str(indoc! {
|
||||
r#"
|
||||
[project]
|
||||
name = "root"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = ["child"]
|
||||
|
||||
[project.scripts]
|
||||
root_cli = "root:main"
|
||||
|
||||
[build-system]
|
||||
requires = ["uv_build>=0.7,<10000"]
|
||||
build-backend = "uv_build"
|
||||
|
||||
[tool.uv.sources]
|
||||
child = { workspace = true, editable = true }
|
||||
|
||||
[tool.uv.workspace]
|
||||
members = ["child"]
|
||||
"#
|
||||
})?;
|
||||
|
||||
let root_src = context.temp_dir.child("src").child("root");
|
||||
root_src.create_dir_all()?;
|
||||
root_src.child("__init__.py").write_str(indoc! {
|
||||
r"
|
||||
ROOT_MESSAGE = 'ROOT'
|
||||
|
||||
def main():
|
||||
import child
|
||||
print(f'{ROOT_MESSAGE} {child.MESSAGE}')
|
||||
"
|
||||
})?;
|
||||
|
||||
let child = context.temp_dir.child("child");
|
||||
child.create_dir_all()?;
|
||||
child.child("pyproject.toml").write_str(indoc! {r#"
|
||||
[project]
|
||||
name = "child"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[build-system]
|
||||
requires = ["uv_build>=0.7,<10000"]
|
||||
build-backend = "uv_build"
|
||||
"#})?;
|
||||
|
||||
let child_src = child.child("src").child("child");
|
||||
child_src.create_dir_all()?;
|
||||
child_src
|
||||
.child("__init__.py")
|
||||
.write_str("MESSAGE = 'OK'\n")?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.tool_install()
|
||||
.arg(context.temp_dir.path())
|
||||
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
|
||||
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
|
||||
.env(EnvVars::PATH, bin_dir.as_os_str()), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 2 packages in [TIME]
|
||||
Prepared 2 packages in [TIME]
|
||||
Installed 2 packages in [TIME]
|
||||
+ child==0.1.0 (from file://[TEMP_DIR]/child)
|
||||
+ root==0.1.0 (from file://[TEMP_DIR]/)
|
||||
Installed 1 executable: root_cli
|
||||
");
|
||||
|
||||
uv_snapshot!(context.filters(), Command::new("root_cli").env(EnvVars::PATH, bin_dir.as_os_str()), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
ROOT OK
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
root_src.child("__init__.py").write_str(indoc! {
|
||||
r"
|
||||
ROOT_MESSAGE = 'CHANGED'
|
||||
|
||||
def main():
|
||||
import child
|
||||
print(f'{ROOT_MESSAGE} {child.MESSAGE}')
|
||||
"
|
||||
})?;
|
||||
|
||||
uv_snapshot!(context.filters(), Command::new("root_cli").env(EnvVars::PATH, bin_dir.as_os_str()), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
ROOT OK
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
child_src
|
||||
.child("__init__.py")
|
||||
.write_str("MESSAGE = 'CHANGED'\n")?;
|
||||
|
||||
uv_snapshot!(context.filters(), Command::new("root_cli").env(EnvVars::PATH, bin_dir.as_os_str()), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
ROOT CHANGED
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_install_with_compatible_build_constraints() -> Result<()> {
|
||||
let context = uv_test::test_context!("3.9")
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
use std::process::Command;
|
||||
|
||||
use anyhow::Result;
|
||||
use assert_cmd::assert::OutputAssertExt;
|
||||
use assert_fs::prelude::*;
|
||||
use indoc::indoc;
|
||||
use insta::assert_snapshot;
|
||||
|
||||
use uv_static::EnvVars;
|
||||
@@ -92,6 +95,425 @@ fn tool_upgrade_empty() {
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_upgrade_preserves_workspace_member_editability() -> Result<()> {
|
||||
let context = uv_test::test_context!("3.12").with_filtered_exe_suffix();
|
||||
let tool_dir = context.temp_dir.child("tools");
|
||||
let bin_dir = context.temp_dir.child("bin");
|
||||
|
||||
let root_pyproject = context.temp_dir.child("pyproject.toml");
|
||||
root_pyproject.write_str(indoc! {
|
||||
r#"
|
||||
[project]
|
||||
name = "root"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = ["child"]
|
||||
|
||||
[project.scripts]
|
||||
root_cli = "root:main"
|
||||
|
||||
[build-system]
|
||||
requires = ["uv_build>=0.7,<10000"]
|
||||
build-backend = "uv_build"
|
||||
|
||||
[tool.uv.sources]
|
||||
child = { workspace = true }
|
||||
|
||||
[tool.uv.workspace]
|
||||
members = ["child"]
|
||||
"#
|
||||
})?;
|
||||
|
||||
let root_src = context.temp_dir.child("src").child("root");
|
||||
root_src.create_dir_all()?;
|
||||
root_src.child("__init__.py").write_str(indoc! {
|
||||
r"
|
||||
def main():
|
||||
import child
|
||||
print(child.MESSAGE)
|
||||
"
|
||||
})?;
|
||||
|
||||
let child = context.temp_dir.child("child");
|
||||
child.create_dir_all()?;
|
||||
child.child("pyproject.toml").write_str(indoc! {r#"
|
||||
[project]
|
||||
name = "child"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[build-system]
|
||||
requires = ["uv_build>=0.7,<10000"]
|
||||
build-backend = "uv_build"
|
||||
"#})?;
|
||||
|
||||
let child_src = child.child("src").child("child");
|
||||
child_src.create_dir_all()?;
|
||||
child_src
|
||||
.child("__init__.py")
|
||||
.write_str("MESSAGE = 'OK'\n")?;
|
||||
|
||||
let status = context
|
||||
.tool_install()
|
||||
.arg(context.temp_dir.path())
|
||||
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
|
||||
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
|
||||
.env(EnvVars::PATH, bin_dir.as_os_str())
|
||||
.status()
|
||||
.expect("failed to run uv tool install");
|
||||
assert!(status.success());
|
||||
|
||||
uv_snapshot!(context.filters(), Command::new("root_cli").env(EnvVars::PATH, bin_dir.as_os_str()), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
OK
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
child_src
|
||||
.child("__init__.py")
|
||||
.write_str("MESSAGE = 'PRE-UPGRADE'\n")?;
|
||||
|
||||
uv_snapshot!(context.filters(), Command::new("root_cli").env(EnvVars::PATH, bin_dir.as_os_str()), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
OK
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
let status = context
|
||||
.tool_upgrade()
|
||||
.arg("root")
|
||||
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
|
||||
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
|
||||
.env(EnvVars::PATH, bin_dir.as_os_str())
|
||||
.status()
|
||||
.expect("failed to run uv tool upgrade");
|
||||
assert!(status.success());
|
||||
|
||||
uv_snapshot!(context.filters(), Command::new("root_cli").env(EnvVars::PATH, bin_dir.as_os_str()), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
OK
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
child_src
|
||||
.child("__init__.py")
|
||||
.write_str("MESSAGE = 'POST-UPGRADE'\n")?;
|
||||
|
||||
uv_snapshot!(context.filters(), Command::new("root_cli").env(EnvVars::PATH, bin_dir.as_os_str()), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
OK
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_upgrade_preserves_mixed_workspace_member_editability() -> Result<()> {
|
||||
let context = uv_test::test_context!("3.12").with_filtered_exe_suffix();
|
||||
let tool_dir = context.temp_dir.child("tools");
|
||||
let bin_dir = context.temp_dir.child("bin");
|
||||
|
||||
let tool_root = context.temp_dir.child("tool-root");
|
||||
tool_root.create_dir_all()?;
|
||||
tool_root.child("pyproject.toml").write_str(indoc! {r#"
|
||||
[project]
|
||||
name = "tool-root"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[project.scripts]
|
||||
root_cli = "tool_root:main"
|
||||
|
||||
[build-system]
|
||||
requires = ["uv_build>=0.7,<10000"]
|
||||
build-backend = "uv_build"
|
||||
"#})?;
|
||||
let tool_root_src = tool_root.child("src").child("tool_root");
|
||||
tool_root_src.create_dir_all()?;
|
||||
tool_root_src.child("__init__.py").write_str(indoc! {
|
||||
r#"
|
||||
def main():
|
||||
import importlib.metadata
|
||||
import other_child
|
||||
|
||||
print(f"{importlib.metadata.version('tool-root')} {other_child.MESSAGE}")
|
||||
"#
|
||||
})?;
|
||||
|
||||
let other_workspace = context.temp_dir.child("other-workspace");
|
||||
other_workspace.create_dir_all()?;
|
||||
other_workspace
|
||||
.child("pyproject.toml")
|
||||
.write_str(indoc! {r#"
|
||||
[project]
|
||||
name = "other-workspace"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = ["other-child"]
|
||||
|
||||
[build-system]
|
||||
requires = ["uv_build>=0.7,<10000"]
|
||||
build-backend = "uv_build"
|
||||
|
||||
[tool.uv.sources]
|
||||
other-child = { workspace = true }
|
||||
|
||||
[tool.uv.workspace]
|
||||
members = ["packages/*"]
|
||||
"#})?;
|
||||
let other_workspace_src = other_workspace.child("src").child("other_workspace");
|
||||
other_workspace_src.create_dir_all()?;
|
||||
other_workspace_src.child("__init__.py").touch()?;
|
||||
|
||||
let other_child = other_workspace.child("packages").child("other-child");
|
||||
other_child.create_dir_all()?;
|
||||
other_child.child("pyproject.toml").write_str(indoc! {r#"
|
||||
[project]
|
||||
name = "other-child"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[build-system]
|
||||
requires = ["uv_build>=0.7,<10000"]
|
||||
build-backend = "uv_build"
|
||||
"#})?;
|
||||
let other_child_src = other_child.child("src").child("other_child");
|
||||
other_child_src.create_dir_all()?;
|
||||
other_child_src
|
||||
.child("__init__.py")
|
||||
.write_str("MESSAGE = 'OK'\n")?;
|
||||
|
||||
let status = context
|
||||
.tool_install()
|
||||
.arg("--with-editable")
|
||||
.arg(other_workspace.path())
|
||||
.arg(tool_root.path())
|
||||
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
|
||||
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
|
||||
.env(EnvVars::PATH, bin_dir.as_os_str())
|
||||
.status()
|
||||
.expect("failed to run uv tool install");
|
||||
assert!(status.success());
|
||||
|
||||
uv_snapshot!(context.filters(), Command::new("root_cli").env(EnvVars::PATH, bin_dir.as_os_str()), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
0.1.0 OK
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
tool_root.child("pyproject.toml").write_str(indoc! {r#"
|
||||
[project]
|
||||
name = "tool-root"
|
||||
version = "0.1.1"
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[project.scripts]
|
||||
root_cli = "tool_root:main"
|
||||
|
||||
[build-system]
|
||||
requires = ["uv_build>=0.7,<10000"]
|
||||
build-backend = "uv_build"
|
||||
"#})?;
|
||||
|
||||
let status = context
|
||||
.tool_upgrade()
|
||||
.arg("tool-root")
|
||||
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
|
||||
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
|
||||
.env(EnvVars::PATH, bin_dir.as_os_str())
|
||||
.status()
|
||||
.expect("failed to run uv tool upgrade");
|
||||
assert!(status.success());
|
||||
|
||||
uv_snapshot!(context.filters(), Command::new("root_cli").env(EnvVars::PATH, bin_dir.as_os_str()), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
0.1.1 OK
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
other_child_src
|
||||
.child("__init__.py")
|
||||
.write_str("MESSAGE = 'POST-UPGRADE'\n")?;
|
||||
|
||||
uv_snapshot!(context.filters(), Command::new("root_cli").env(EnvVars::PATH, bin_dir.as_os_str()), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
0.1.1 POST-UPGRADE
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_upgrade_preserves_mixed_workspace_member_non_editability() -> Result<()> {
|
||||
let context = uv_test::test_context!("3.12").with_filtered_exe_suffix();
|
||||
let tool_dir = context.temp_dir.child("tools");
|
||||
let bin_dir = context.temp_dir.child("bin");
|
||||
|
||||
let tool_root = context.temp_dir.child("tool-root");
|
||||
tool_root.create_dir_all()?;
|
||||
tool_root.child("pyproject.toml").write_str(indoc! {r#"
|
||||
[project]
|
||||
name = "tool-root"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[project.scripts]
|
||||
root_cli = "tool_root:main"
|
||||
|
||||
[build-system]
|
||||
requires = ["uv_build>=0.7,<10000"]
|
||||
build-backend = "uv_build"
|
||||
"#})?;
|
||||
let tool_root_src = tool_root.child("src").child("tool_root");
|
||||
tool_root_src.create_dir_all()?;
|
||||
tool_root_src.child("__init__.py").write_str(indoc! {
|
||||
r#"
|
||||
def main():
|
||||
import importlib.metadata
|
||||
import other_child
|
||||
|
||||
print(f"{importlib.metadata.version('tool-root')} {other_child.MESSAGE}")
|
||||
"#
|
||||
})?;
|
||||
|
||||
let other_workspace = context.temp_dir.child("other-workspace");
|
||||
other_workspace.create_dir_all()?;
|
||||
other_workspace
|
||||
.child("pyproject.toml")
|
||||
.write_str(indoc! {r#"
|
||||
[project]
|
||||
name = "other-workspace"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = ["other-child"]
|
||||
|
||||
[build-system]
|
||||
requires = ["uv_build>=0.7,<10000"]
|
||||
build-backend = "uv_build"
|
||||
|
||||
[tool.uv.sources]
|
||||
other-child = { workspace = true }
|
||||
|
||||
[tool.uv.workspace]
|
||||
members = ["packages/*"]
|
||||
"#})?;
|
||||
let other_workspace_src = other_workspace.child("src").child("other_workspace");
|
||||
other_workspace_src.create_dir_all()?;
|
||||
other_workspace_src.child("__init__.py").touch()?;
|
||||
|
||||
let other_child = other_workspace.child("packages").child("other-child");
|
||||
other_child.create_dir_all()?;
|
||||
other_child.child("pyproject.toml").write_str(indoc! {r#"
|
||||
[project]
|
||||
name = "other-child"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[build-system]
|
||||
requires = ["uv_build>=0.7,<10000"]
|
||||
build-backend = "uv_build"
|
||||
"#})?;
|
||||
let other_child_src = other_child.child("src").child("other_child");
|
||||
other_child_src.create_dir_all()?;
|
||||
other_child_src
|
||||
.child("__init__.py")
|
||||
.write_str("MESSAGE = 'OK'\n")?;
|
||||
|
||||
let status = context
|
||||
.tool_install()
|
||||
.arg("--editable")
|
||||
.arg("--with")
|
||||
.arg(other_workspace.path())
|
||||
.arg(tool_root.path())
|
||||
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
|
||||
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
|
||||
.env(EnvVars::PATH, bin_dir.as_os_str())
|
||||
.status()
|
||||
.expect("failed to run uv tool install");
|
||||
assert!(status.success());
|
||||
|
||||
uv_snapshot!(context.filters(), Command::new("root_cli").env(EnvVars::PATH, bin_dir.as_os_str()), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
0.1.0 OK
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
tool_root.child("pyproject.toml").write_str(indoc! {r#"
|
||||
[project]
|
||||
name = "tool-root"
|
||||
version = "0.1.1"
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[project.scripts]
|
||||
root_cli = "tool_root:main"
|
||||
|
||||
[build-system]
|
||||
requires = ["uv_build>=0.7,<10000"]
|
||||
build-backend = "uv_build"
|
||||
"#})?;
|
||||
|
||||
let status = context
|
||||
.tool_upgrade()
|
||||
.arg("tool-root")
|
||||
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
|
||||
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
|
||||
.env(EnvVars::PATH, bin_dir.as_os_str())
|
||||
.status()
|
||||
.expect("failed to run uv tool upgrade");
|
||||
assert!(status.success());
|
||||
|
||||
uv_snapshot!(context.filters(), Command::new("root_cli").env(EnvVars::PATH, bin_dir.as_os_str()), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
0.1.1 OK
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
other_child_src
|
||||
.child("__init__.py")
|
||||
.write_str("MESSAGE = 'POST-UPGRADE'\n")?;
|
||||
|
||||
uv_snapshot!(context.filters(), Command::new("root_cli").env(EnvVars::PATH, bin_dir.as_os_str()), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
0.1.1 OK
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_upgrade_name() {
|
||||
let context = uv_test::test_context!("3.12")
|
||||
|
||||
Reference in New Issue
Block a user