Cache workspace discovery (#12096)

Reduce the overhead of `uv run` in large workspaces. Instead of
re-discovering the entire workspace each time we resolve the metadata of
a member, we can the discovered set of workspace members. Care needs to
be taken to not cache the discovery for `uv init`, `uv add` and `uv
remove`, which change the definitions of workspace members.

Below is apache airflow e3fe06382df4b19f2c0de40ce7c0bdc726754c74 `uv run
python` with a minimal payload. With this change, we avoid a ~350ms
overhead of each `uv run` invocation.

```
$ hyperfine --warmup 2 \
    "uv run --no-dev python -c \"print('hi')\"" \
    "uv-profiling run --no-dev python -c \"print('hi')\""
Benchmark 1: uv run --no-dev python -c "print('hi')"
  Time (mean ± σ):     492.6 ms ±   7.0 ms    [User: 393.2 ms, System: 97.1 ms]
  Range (min … max):   482.3 ms … 501.5 ms    10 runs
 
Benchmark 2: uv-profiling run --no-dev python -c "print('hi')"
  Time (mean ± σ):     129.7 ms ±   2.5 ms    [User: 105.4 ms, System: 23.2 ms]
  Range (min … max):   126.0 ms … 136.1 ms    22 runs
 
Summary
  uv-profiling run --no-dev python -c "print('hi')" ran
    3.80 ± 0.09 times faster than uv run --no-dev python -c "print('hi')"
```

The profile after those change below. We still spend a large chunk in
toml parsing (both `uv.lock` and `pyproject.toml`), but it's not
excessive anymore.


![image](https://github.com/user-attachments/assets/6fe78510-7e25-48ee-8a6d-220ee98ad120)
This commit is contained in:
konsti 2025-03-10 22:03:30 +01:00 committed by GitHub
parent 15663eab26
commit e843433b07
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
36 changed files with 381 additions and 154 deletions

4
Cargo.lock generated
View File

@ -4669,6 +4669,7 @@ dependencies = [
"uv-python", "uv-python",
"uv-resolver", "uv-resolver",
"uv-types", "uv-types",
"uv-workspace",
] ]
[[package]] [[package]]
@ -4745,6 +4746,7 @@ dependencies = [
"uv-types", "uv-types",
"uv-virtualenv", "uv-virtualenv",
"uv-warnings", "uv-warnings",
"uv-workspace",
] ]
[[package]] [[package]]
@ -4997,6 +4999,7 @@ dependencies = [
"uv-resolver", "uv-resolver",
"uv-types", "uv-types",
"uv-version", "uv-version",
"uv-workspace",
] ]
[[package]] [[package]]
@ -5778,6 +5781,7 @@ dependencies = [
"uv-pep508", "uv-pep508",
"uv-pypi-types", "uv-pypi-types",
"uv-python", "uv-python",
"uv-workspace",
] ]
[[package]] [[package]]

View File

@ -45,6 +45,7 @@ uv-pypi-types = { workspace = true }
uv-python = { workspace = true } uv-python = { workspace = true }
uv-resolver = { workspace = true } uv-resolver = { workspace = true }
uv-types = { workspace = true } uv-types = { workspace = true }
uv-workspace = { workspace = true }
anyhow = { workspace = true } anyhow = { workspace = true }
codspeed-criterion-compat = { version = "2.7.2", default-features = false, optional = true } codspeed-criterion-compat = { version = "2.7.2", default-features = false, optional = true }

View File

@ -103,6 +103,7 @@ mod resolver {
Resolver, ResolverEnvironment, ResolverOutput, Resolver, ResolverEnvironment, ResolverOutput,
}; };
use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy}; use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy};
use uv_workspace::WorkspaceCache;
static MARKERS: LazyLock<MarkerEnvironment> = LazyLock::new(|| { static MARKERS: LazyLock<MarkerEnvironment> = LazyLock::new(|| {
MarkerEnvironment::try_from(MarkerEnvironmentBuilder { MarkerEnvironment::try_from(MarkerEnvironmentBuilder {
@ -161,6 +162,7 @@ mod resolver {
let sources = SourceStrategy::default(); let sources = SourceStrategy::default();
let dependency_metadata = DependencyMetadata::default(); let dependency_metadata = DependencyMetadata::default();
let conflicts = Conflicts::empty(); let conflicts = Conflicts::empty();
let workspace_cache = WorkspaceCache::default();
let python_requirement = if universal { let python_requirement = if universal {
PythonRequirement::from_requires_python( PythonRequirement::from_requires_python(
@ -188,6 +190,7 @@ mod resolver {
&hashes, &hashes,
exclude_newer, exclude_newer,
sources, sources,
workspace_cache,
concurrency, concurrency,
PreviewMode::Enabled, PreviewMode::Enabled,
); );

View File

@ -29,6 +29,7 @@ uv-static = { workspace = true }
uv-types = { workspace = true } uv-types = { workspace = true }
uv-virtualenv = { workspace = true } uv-virtualenv = { workspace = true }
uv-warnings = { workspace = true } uv-warnings = { workspace = true }
uv-workspace = { workspace = true }
anstream = { workspace = true } anstream = { workspace = true }
fs-err = { workspace = true } fs-err = { workspace = true }

View File

@ -38,6 +38,7 @@ use uv_python::{Interpreter, PythonEnvironment};
use uv_static::EnvVars; use uv_static::EnvVars;
use uv_types::{AnyErrorBuild, BuildContext, BuildIsolation, BuildStack, SourceBuildTrait}; use uv_types::{AnyErrorBuild, BuildContext, BuildIsolation, BuildStack, SourceBuildTrait};
use uv_warnings::warn_user_once; use uv_warnings::warn_user_once;
use uv_workspace::WorkspaceCache;
pub use crate::error::{Error, MissingHeaderCause}; pub use crate::error::{Error, MissingHeaderCause};
@ -269,6 +270,7 @@ impl SourceBuild {
version_id: Option<&str>, version_id: Option<&str>,
locations: &IndexLocations, locations: &IndexLocations,
source_strategy: SourceStrategy, source_strategy: SourceStrategy,
workspace_cache: &WorkspaceCache,
config_settings: ConfigSettings, config_settings: ConfigSettings,
build_isolation: BuildIsolation<'_>, build_isolation: BuildIsolation<'_>,
build_stack: &BuildStack, build_stack: &BuildStack,
@ -294,6 +296,7 @@ impl SourceBuild {
fallback_package_name, fallback_package_name,
locations, locations,
source_strategy, source_strategy,
workspace_cache,
&default_backend, &default_backend,
) )
.await .await
@ -396,6 +399,7 @@ impl SourceBuild {
version_id, version_id,
locations, locations,
source_strategy, source_strategy,
workspace_cache,
build_stack, build_stack,
build_kind, build_kind,
level, level,
@ -466,6 +470,7 @@ impl SourceBuild {
package_name: Option<&PackageName>, package_name: Option<&PackageName>,
locations: &IndexLocations, locations: &IndexLocations,
source_strategy: SourceStrategy, source_strategy: SourceStrategy,
workspace_cache: &WorkspaceCache,
default_backend: &Pep517Backend, default_backend: &Pep517Backend,
) -> Result<(Pep517Backend, Option<Project>), Box<Error>> { ) -> Result<(Pep517Backend, Option<Project>), Box<Error>> {
match fs::read_to_string(source_tree.join("pyproject.toml")) { match fs::read_to_string(source_tree.join("pyproject.toml")) {
@ -496,6 +501,7 @@ impl SourceBuild {
install_path, install_path,
locations, locations,
source_strategy, source_strategy,
workspace_cache,
) )
.await .await
.map_err(Error::Lowering)?; .map_err(Error::Lowering)?;
@ -857,6 +863,7 @@ async fn create_pep517_build_environment(
version_id: Option<&str>, version_id: Option<&str>,
locations: &IndexLocations, locations: &IndexLocations,
source_strategy: SourceStrategy, source_strategy: SourceStrategy,
workspace_cache: &WorkspaceCache,
build_stack: &BuildStack, build_stack: &BuildStack,
build_kind: BuildKind, build_kind: BuildKind,
level: BuildOutput, level: BuildOutput,
@ -957,6 +964,7 @@ async fn create_pep517_build_environment(
install_path, install_path,
locations, locations,
source_strategy, source_strategy,
workspace_cache,
) )
.await .await
.map_err(Error::Lowering)?; .map_err(Error::Lowering)?;

View File

@ -34,6 +34,7 @@ uv-python = { workspace = true }
uv-resolver = { workspace = true } uv-resolver = { workspace = true }
uv-types = { workspace = true } uv-types = { workspace = true }
uv-version = { workspace = true } uv-version = { workspace = true }
uv-workspace = { workspace = true }
anyhow = { workspace = true } anyhow = { workspace = true }
futures = { workspace = true } futures = { workspace = true }

View File

@ -38,6 +38,7 @@ use uv_types::{
AnyErrorBuild, BuildContext, BuildIsolation, BuildStack, EmptyInstalledPackages, HashStrategy, AnyErrorBuild, BuildContext, BuildIsolation, BuildStack, EmptyInstalledPackages, HashStrategy,
InFlight, InFlight,
}; };
use uv_workspace::WorkspaceCache;
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum BuildDispatchError { pub enum BuildDispatchError {
@ -94,6 +95,7 @@ pub struct BuildDispatch<'a> {
source_build_context: SourceBuildContext, source_build_context: SourceBuildContext,
build_extra_env_vars: FxHashMap<OsString, OsString>, build_extra_env_vars: FxHashMap<OsString, OsString>,
sources: SourceStrategy, sources: SourceStrategy,
workspace_cache: WorkspaceCache,
concurrency: Concurrency, concurrency: Concurrency,
preview: PreviewMode, preview: PreviewMode,
} }
@ -116,6 +118,7 @@ impl<'a> BuildDispatch<'a> {
hasher: &'a HashStrategy, hasher: &'a HashStrategy,
exclude_newer: Option<ExcludeNewer>, exclude_newer: Option<ExcludeNewer>,
sources: SourceStrategy, sources: SourceStrategy,
workspace_cache: WorkspaceCache,
concurrency: Concurrency, concurrency: Concurrency,
preview: PreviewMode, preview: PreviewMode,
) -> Self { ) -> Self {
@ -137,8 +140,8 @@ impl<'a> BuildDispatch<'a> {
exclude_newer, exclude_newer,
source_build_context: SourceBuildContext::default(), source_build_context: SourceBuildContext::default(),
build_extra_env_vars: FxHashMap::default(), build_extra_env_vars: FxHashMap::default(),
sources, sources,
workspace_cache,
concurrency, concurrency,
preview, preview,
} }
@ -200,6 +203,10 @@ impl BuildContext for BuildDispatch<'_> {
self.index_locations self.index_locations
} }
fn workspace_cache(&self) -> &WorkspaceCache {
&self.workspace_cache
}
async fn resolve<'data>( async fn resolve<'data>(
&'data self, &'data self,
requirements: &'data [Requirement], requirements: &'data [Requirement],
@ -417,6 +424,7 @@ impl BuildContext for BuildDispatch<'_> {
version_id, version_id,
self.index_locations, self.index_locations,
sources, sources,
self.workspace_cache(),
self.config_settings.clone(), self.config_settings.clone(),
self.build_isolation, self.build_isolation,
&build_stack, &build_stack,

View File

@ -5,7 +5,9 @@ use uv_configuration::SourceStrategy;
use uv_distribution_types::IndexLocations; use uv_distribution_types::IndexLocations;
use uv_normalize::PackageName; use uv_normalize::PackageName;
use uv_workspace::pyproject::ToolUvSources; use uv_workspace::pyproject::ToolUvSources;
use uv_workspace::{DiscoveryOptions, MemberDiscovery, ProjectWorkspace, Workspace}; use uv_workspace::{
DiscoveryOptions, MemberDiscovery, ProjectWorkspace, Workspace, WorkspaceCache,
};
use crate::metadata::{LoweredRequirement, MetadataError}; use crate::metadata::{LoweredRequirement, MetadataError};
@ -37,6 +39,7 @@ impl BuildRequires {
install_path: &Path, install_path: &Path,
locations: &IndexLocations, locations: &IndexLocations,
sources: SourceStrategy, sources: SourceStrategy,
cache: &WorkspaceCache,
) -> Result<Self, MetadataError> { ) -> Result<Self, MetadataError> {
let discovery = match sources { let discovery = match sources {
SourceStrategy::Enabled => DiscoveryOptions::default(), SourceStrategy::Enabled => DiscoveryOptions::default(),
@ -48,7 +51,7 @@ impl BuildRequires {
// TODO(konsti): Cache workspace discovery. // TODO(konsti): Cache workspace discovery.
let Some(project_workspace) = let Some(project_workspace) =
ProjectWorkspace::from_maybe_project_root(install_path, &discovery).await? ProjectWorkspace::from_maybe_project_root(install_path, &discovery, cache).await?
else { else {
return Ok(Self::from_metadata23(metadata)); return Ok(Self::from_metadata23(metadata));
}; };

View File

@ -9,7 +9,7 @@ use uv_normalize::{ExtraName, GroupName, PackageName};
use uv_pep440::{Version, VersionSpecifiers}; use uv_pep440::{Version, VersionSpecifiers};
use uv_pypi_types::{HashDigests, ResolutionMetadata}; use uv_pypi_types::{HashDigests, ResolutionMetadata};
use uv_workspace::dependency_groups::DependencyGroupError; use uv_workspace::dependency_groups::DependencyGroupError;
use uv_workspace::WorkspaceError; use uv_workspace::{WorkspaceCache, WorkspaceError};
pub use crate::metadata::build_requires::BuildRequires; pub use crate::metadata::build_requires::BuildRequires;
pub use crate::metadata::lowering::LoweredRequirement; pub use crate::metadata::lowering::LoweredRequirement;
@ -80,6 +80,7 @@ impl Metadata {
git_source: Option<&GitWorkspaceMember<'_>>, git_source: Option<&GitWorkspaceMember<'_>>,
locations: &IndexLocations, locations: &IndexLocations,
sources: SourceStrategy, sources: SourceStrategy,
cache: &WorkspaceCache,
) -> Result<Self, MetadataError> { ) -> Result<Self, MetadataError> {
// Lower the requirements. // Lower the requirements.
let requires_dist = uv_pypi_types::RequiresDist { let requires_dist = uv_pypi_types::RequiresDist {
@ -100,6 +101,7 @@ impl Metadata {
git_source, git_source,
locations, locations,
sources, sources,
cache,
) )
.await?; .await?;

View File

@ -10,7 +10,7 @@ use uv_normalize::{ExtraName, GroupName, PackageName, DEV_DEPENDENCIES};
use uv_pep508::MarkerTree; use uv_pep508::MarkerTree;
use uv_workspace::dependency_groups::FlatDependencyGroups; use uv_workspace::dependency_groups::FlatDependencyGroups;
use uv_workspace::pyproject::{Sources, ToolUvSources}; use uv_workspace::pyproject::{Sources, ToolUvSources};
use uv_workspace::{DiscoveryOptions, MemberDiscovery, ProjectWorkspace}; use uv_workspace::{DiscoveryOptions, MemberDiscovery, ProjectWorkspace, WorkspaceCache};
use crate::metadata::{GitWorkspaceMember, LoweredRequirement, MetadataError}; use crate::metadata::{GitWorkspaceMember, LoweredRequirement, MetadataError};
use crate::Metadata; use crate::Metadata;
@ -49,6 +49,7 @@ impl RequiresDist {
git_member: Option<&GitWorkspaceMember<'_>>, git_member: Option<&GitWorkspaceMember<'_>>,
locations: &IndexLocations, locations: &IndexLocations,
sources: SourceStrategy, sources: SourceStrategy,
cache: &WorkspaceCache,
) -> Result<Self, MetadataError> { ) -> Result<Self, MetadataError> {
// TODO(konsti): Cache workspace discovery. // TODO(konsti): Cache workspace discovery.
let discovery_options = DiscoveryOptions { let discovery_options = DiscoveryOptions {
@ -57,6 +58,7 @@ impl RequiresDist {
.fetch_root .fetch_root
.parent() .parent()
.expect("git checkout has a parent") .expect("git checkout has a parent")
.to_path_buf()
}), }),
members: match sources { members: match sources {
SourceStrategy::Enabled => MemberDiscovery::default(), SourceStrategy::Enabled => MemberDiscovery::default(),
@ -64,7 +66,8 @@ impl RequiresDist {
}, },
}; };
let Some(project_workspace) = let Some(project_workspace) =
ProjectWorkspace::from_maybe_project_root(install_path, &discovery_options).await? ProjectWorkspace::from_maybe_project_root(install_path, &discovery_options, cache)
.await?
else { else {
return Ok(Self::from_metadata23(metadata)); return Ok(Self::from_metadata23(metadata));
}; };
@ -475,7 +478,7 @@ mod test {
use uv_normalize::PackageName; use uv_normalize::PackageName;
use uv_pep508::Requirement; use uv_pep508::Requirement;
use uv_workspace::pyproject::PyProjectToml; use uv_workspace::pyproject::PyProjectToml;
use uv_workspace::{DiscoveryOptions, ProjectWorkspace}; use uv_workspace::{DiscoveryOptions, ProjectWorkspace, WorkspaceCache};
use crate::metadata::requires_dist::FlatRequiresDist; use crate::metadata::requires_dist::FlatRequiresDist;
use crate::RequiresDist; use crate::RequiresDist;
@ -491,9 +494,10 @@ mod test {
.context("metadata field project not found")?, .context("metadata field project not found")?,
&pyproject_toml, &pyproject_toml,
&DiscoveryOptions { &DiscoveryOptions {
stop_discovery_at: Some(path), stop_discovery_at: Some(path.to_path_buf()),
..DiscoveryOptions::default() ..DiscoveryOptions::default()
}, },
&WorkspaceCache::default(),
) )
.await?; .await?;
let requires_dist = uv_pypi_types::RequiresDist::parse_pyproject_toml(contents)?; let requires_dist = uv_pypi_types::RequiresDist::parse_pyproject_toml(contents)?;

View File

@ -1171,6 +1171,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
None, None,
self.build_context.locations(), self.build_context.locations(),
self.build_context.sources(), self.build_context.sources(),
self.build_context.workspace_cache(),
) )
.await?, .await?,
)); ));
@ -1223,6 +1224,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
None, None,
self.build_context.locations(), self.build_context.locations(),
self.build_context.sources(), self.build_context.sources(),
self.build_context.workspace_cache(),
) )
.await?, .await?,
)); ));
@ -1271,6 +1273,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
None, None,
self.build_context.locations(), self.build_context.locations(),
self.build_context.sources(), self.build_context.sources(),
self.build_context.workspace_cache(),
) )
.await?, .await?,
)); ));
@ -1328,6 +1331,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
None, None,
self.build_context.locations(), self.build_context.locations(),
self.build_context.sources(), self.build_context.sources(),
self.build_context.workspace_cache(),
) )
.await?, .await?,
)) ))
@ -1395,6 +1399,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
None, None,
self.build_context.locations(), self.build_context.locations(),
self.build_context.sources(), self.build_context.sources(),
self.build_context.workspace_cache(),
) )
.await?; .await?;
Ok(Some(requires_dist)) Ok(Some(requires_dist))
@ -1653,6 +1658,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
Some(&git_member), Some(&git_member),
self.build_context.locations(), self.build_context.locations(),
self.build_context.sources(), self.build_context.sources(),
self.build_context.workspace_cache(),
) )
.await?, .await?,
)); ));
@ -1685,6 +1691,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
Some(&git_member), Some(&git_member),
self.build_context.locations(), self.build_context.locations(),
self.build_context.sources(), self.build_context.sources(),
self.build_context.workspace_cache(),
) )
.await?, .await?,
)); ));
@ -1736,6 +1743,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
Some(&git_member), Some(&git_member),
self.build_context.locations(), self.build_context.locations(),
self.build_context.sources(), self.build_context.sources(),
self.build_context.workspace_cache(),
) )
.await?, .await?,
)); ));
@ -1793,6 +1801,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
Some(&git_member), Some(&git_member),
self.build_context.locations(), self.build_context.locations(),
self.build_context.sources(), self.build_context.sources(),
self.build_context.workspace_cache(),
) )
.await?, .await?,
)) ))

View File

@ -27,6 +27,7 @@ uv-pep440 = { workspace = true }
uv-pep508 = { workspace = true } uv-pep508 = { workspace = true }
uv-pypi-types = { workspace = true } uv-pypi-types = { workspace = true }
uv-python = { workspace = true } uv-python = { workspace = true }
uv-workspace = { workspace = true }
anyhow = { workspace = true } anyhow = { workspace = true }
rustc-hash = { workspace = true } rustc-hash = { workspace = true }

View File

@ -17,6 +17,7 @@ use uv_git::GitResolver;
use uv_pep508::PackageName; use uv_pep508::PackageName;
use uv_pypi_types::Requirement; use uv_pypi_types::Requirement;
use uv_python::{Interpreter, PythonEnvironment}; use uv_python::{Interpreter, PythonEnvironment};
use uv_workspace::WorkspaceCache;
/// Avoids cyclic crate dependencies between resolver, installer and builder. /// Avoids cyclic crate dependencies between resolver, installer and builder.
/// ///
@ -88,6 +89,9 @@ pub trait BuildContext {
/// The index locations being searched. /// The index locations being searched.
fn locations(&self) -> &IndexLocations; fn locations(&self) -> &IndexLocations;
/// Workspace discovery caching.
fn workspace_cache(&self) -> &WorkspaceCache;
/// Resolve the given requirements into a ready-to-install set of package versions. /// Resolve the given requirements into a ready-to-install set of package versions.
fn resolve<'a>( fn resolve<'a>(
&'a self, &'a self,

View File

@ -1,6 +1,6 @@
pub use workspace::{ pub use workspace::{
DiscoveryOptions, MemberDiscovery, ProjectWorkspace, VirtualProject, Workspace, WorkspaceError, DiscoveryOptions, MemberDiscovery, ProjectWorkspace, VirtualProject, Workspace, WorkspaceCache,
WorkspaceMember, WorkspaceError, WorkspaceMember,
}; };
pub mod dependency_groups; pub mod dependency_groups;

View File

@ -1,11 +1,13 @@
//! Resolve the current [`ProjectWorkspace`] or [`Workspace`]. //! Resolve the current [`ProjectWorkspace`] or [`Workspace`].
use std::collections::BTreeMap; use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use glob::{glob, GlobError, PatternError}; use glob::{glob, GlobError, PatternError};
use rustc_hash::FxHashSet; use rustc_hash::{FxHashMap, FxHashSet};
use tracing::{debug, trace, warn}; use tracing::{debug, trace, warn};
use uv_distribution_types::Index; use uv_distribution_types::Index;
use uv_fs::{Simplified, CWD}; use uv_fs::{Simplified, CWD};
use uv_normalize::{GroupName, PackageName, DEV_DEPENDENCIES}; use uv_normalize::{GroupName, PackageName, DEV_DEPENDENCIES};
@ -22,6 +24,24 @@ use crate::pyproject::{
Project, PyProjectToml, PyprojectTomlError, Sources, ToolUvSources, ToolUvWorkspace, Project, PyProjectToml, PyprojectTomlError, Sources, ToolUvSources, ToolUvWorkspace,
}; };
type WorkspaceMembers = Arc<BTreeMap<PackageName, WorkspaceMember>>;
/// Cache key for workspace discovery.
///
/// Given this key, the discovered workspace member list is the same.
#[derive(Debug, Default, Clone, Hash, PartialEq, Eq)]
struct WorkspaceCacheKey {
workspace_root: PathBuf,
discovery_options: DiscoveryOptions,
}
/// Cache for workspace discovery.
///
/// Avoid re-reading the `pyproject.toml` files in a workspace for each member by caching the
/// workspace members by their workspace root.
#[derive(Debug, Default, Clone)]
pub struct WorkspaceCache(Arc<Mutex<FxHashMap<WorkspaceCacheKey, WorkspaceMembers>>>);
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
pub enum WorkspaceError { pub enum WorkspaceError {
// Workspace structure errors. // Workspace structure errors.
@ -58,23 +78,23 @@ pub enum WorkspaceError {
Normalize(#[source] std::io::Error), Normalize(#[source] std::io::Error),
} }
#[derive(Debug, Default, Clone)] #[derive(Debug, Default, Clone, Hash, PartialEq, Eq)]
pub enum MemberDiscovery<'a> { pub enum MemberDiscovery {
/// Discover all workspace members. /// Discover all workspace members.
#[default] #[default]
All, All,
/// Don't discover any workspace members. /// Don't discover any workspace members.
None, None,
/// Discover workspace members, but ignore the given paths. /// Discover workspace members, but ignore the given paths.
Ignore(FxHashSet<&'a Path>), Ignore(BTreeSet<PathBuf>),
} }
#[derive(Debug, Default, Clone)] #[derive(Debug, Default, Clone, Hash, PartialEq, Eq)]
pub struct DiscoveryOptions<'a> { pub struct DiscoveryOptions {
/// The path to stop discovery at. /// The path to stop discovery at.
pub stop_discovery_at: Option<&'a Path>, pub stop_discovery_at: Option<PathBuf>,
/// The strategy to use when discovering workspace members. /// The strategy to use when discovering workspace members.
pub members: MemberDiscovery<'a>, pub members: MemberDiscovery,
} }
/// A workspace, consisting of a root directory and members. See [`ProjectWorkspace`]. /// A workspace, consisting of a root directory and members. See [`ProjectWorkspace`].
@ -87,7 +107,7 @@ pub struct Workspace {
/// the `uv.tool.workspace`, or the `pyproject.toml` in an implicit single workspace project. /// the `uv.tool.workspace`, or the `pyproject.toml` in an implicit single workspace project.
install_path: PathBuf, install_path: PathBuf,
/// The members of the workspace. /// The members of the workspace.
packages: BTreeMap<PackageName, WorkspaceMember>, packages: WorkspaceMembers,
/// The sources table from the workspace `pyproject.toml`. /// The sources table from the workspace `pyproject.toml`.
/// ///
/// This table is overridden by the project sources. /// This table is overridden by the project sources.
@ -124,7 +144,8 @@ impl Workspace {
/// ``` /// ```
pub async fn discover( pub async fn discover(
path: &Path, path: &Path,
options: &DiscoveryOptions<'_>, options: &DiscoveryOptions,
cache: &WorkspaceCache,
) -> Result<Workspace, WorkspaceError> { ) -> Result<Workspace, WorkspaceError> {
let path = std::path::absolute(path) let path = std::path::absolute(path)
.map_err(WorkspaceError::Normalize)? .map_err(WorkspaceError::Normalize)?
@ -211,6 +232,7 @@ impl Workspace {
workspace_pyproject_toml, workspace_pyproject_toml,
current_project, current_project,
options, options,
cache,
) )
.await .await
} }
@ -237,7 +259,7 @@ impl Workspace {
pyproject_toml: PyProjectToml, pyproject_toml: PyProjectToml,
) -> Option<Self> { ) -> Option<Self> {
let mut packages = self.packages; let mut packages = self.packages;
let member = packages.get_mut(package_name)?; let member = Arc::make_mut(&mut packages).get_mut(package_name)?;
if member.root == self.install_path { if member.root == self.install_path {
// If the member is also the workspace root, update _both_ the member entry and the // If the member is also the workspace root, update _both_ the member entry and the
@ -656,57 +678,113 @@ impl Workspace {
workspace_definition: ToolUvWorkspace, workspace_definition: ToolUvWorkspace,
workspace_pyproject_toml: PyProjectToml, workspace_pyproject_toml: PyProjectToml,
current_project: Option<WorkspaceMember>, current_project: Option<WorkspaceMember>,
options: &DiscoveryOptions<'_>, options: &DiscoveryOptions,
cache: &WorkspaceCache,
) -> Result<Workspace, WorkspaceError> { ) -> Result<Workspace, WorkspaceError> {
let cache_key = WorkspaceCacheKey {
workspace_root: workspace_root.clone(),
discovery_options: options.clone(),
};
let cache_entry = {
// Acquire the lock for the minimal required region
let cache = cache.0.lock().expect("there was a panic in another thread");
cache.get(&cache_key).cloned()
};
let mut workspace_members = if let Some(workspace_members) = cache_entry {
trace!(
"Cached workspace members for: `{}`",
&workspace_root.simplified_display()
);
workspace_members
} else {
trace!(
"Discovering workspace members for: `{}`",
&workspace_root.simplified_display()
);
let workspace_members = Self::collect_members_only(
&workspace_root,
&workspace_definition,
&workspace_pyproject_toml,
options,
)
.await?;
{
// Acquire the lock for the minimal required region
let mut cache = cache.0.lock().expect("there was a panic in another thread");
cache.insert(cache_key, Arc::new(workspace_members.clone()));
}
Arc::new(workspace_members)
};
// For the cases such as `MemberDiscovery::None`, add the current project if missing.
if let Some(root_member) = current_project {
if !workspace_members.contains_key(&root_member.project.name) {
debug!(
"Adding current workspace member: `{}`",
root_member.root.simplified_display()
);
Arc::make_mut(&mut workspace_members)
.insert(root_member.project.name.clone(), root_member);
}
}
let workspace_sources = workspace_pyproject_toml
.tool
.clone()
.and_then(|tool| tool.uv)
.and_then(|uv| uv.sources)
.map(ToolUvSources::into_inner)
.unwrap_or_default();
let workspace_indexes = workspace_pyproject_toml
.tool
.clone()
.and_then(|tool| tool.uv)
.and_then(|uv| uv.index)
.unwrap_or_default();
Ok(Workspace {
install_path: workspace_root,
packages: workspace_members,
sources: workspace_sources,
indexes: workspace_indexes,
pyproject_toml: workspace_pyproject_toml,
})
}
async fn collect_members_only(
workspace_root: &PathBuf,
workspace_definition: &ToolUvWorkspace,
workspace_pyproject_toml: &PyProjectToml,
options: &DiscoveryOptions,
) -> Result<BTreeMap<PackageName, WorkspaceMember>, WorkspaceError> {
let mut workspace_members = BTreeMap::new(); let mut workspace_members = BTreeMap::new();
// Avoid reading a `pyproject.toml` more than once. // Avoid reading a `pyproject.toml` more than once.
let mut seen = FxHashSet::default(); let mut seen = FxHashSet::default();
// Add the project at the workspace root, if it exists and if it's distinct from the current // Add the project at the workspace root, if it exists and if it's distinct from the current
// project. // project. If it is the current project, it is added as such in the next step.
if current_project if let Some(project) = &workspace_pyproject_toml.project {
.as_ref() let pyproject_path = workspace_root.join("pyproject.toml");
.map(|root_member| root_member.root != workspace_root) let contents = fs_err::read_to_string(&pyproject_path)?;
.unwrap_or(true) let pyproject_toml = PyProjectToml::from_string(contents)
{ .map_err(|err| WorkspaceError::Toml(pyproject_path.clone(), Box::new(err)))?;
if let Some(project) = &workspace_pyproject_toml.project {
let pyproject_path = workspace_root.join("pyproject.toml");
let contents = fs_err::read_to_string(&pyproject_path)?;
let pyproject_toml = PyProjectToml::from_string(contents)
.map_err(|err| WorkspaceError::Toml(pyproject_path.clone(), Box::new(err)))?;
debug!(
"Adding root workspace member: `{}`",
workspace_root.simplified_display()
);
seen.insert(workspace_root.clone());
if let Some(existing) = workspace_members.insert(
project.name.clone(),
WorkspaceMember {
root: workspace_root.clone(),
project: project.clone(),
pyproject_toml,
},
) {
return Err(WorkspaceError::DuplicatePackage {
name: project.name.clone(),
first: existing.root.clone(),
second: workspace_root,
});
}
};
}
// The current project is a workspace member, especially in a single project workspace.
if let Some(root_member) = current_project {
debug!( debug!(
"Adding current workspace member: `{}`", "Adding root workspace member: `{}`",
root_member.root.simplified_display() workspace_root.simplified_display()
); );
seen.insert(root_member.root.clone()); seen.insert(workspace_root.clone());
workspace_members.insert(root_member.project.name.clone(), root_member); workspace_members.insert(
project.name.clone(),
WorkspaceMember {
root: workspace_root.clone(),
project: project.clone(),
pyproject_toml,
},
);
} }
// Add all other workspace members. // Add all other workspace members.
@ -744,8 +822,7 @@ impl Workspace {
} }
// If the member is excluded, ignore it. // If the member is excluded, ignore it.
if is_excluded_from_workspace(&member_root, &workspace_root, &workspace_definition)? if is_excluded_from_workspace(&member_root, workspace_root, workspace_definition)? {
{
debug!( debug!(
"Ignoring workspace member: `{}`", "Ignoring workspace member: `{}`",
member_root.simplified_display() member_root.simplified_display()
@ -842,7 +919,7 @@ impl Workspace {
// Test for nested workspaces. // Test for nested workspaces.
for member in workspace_members.values() { for member in workspace_members.values() {
if member.root() != &workspace_root if member.root() != workspace_root
&& member && member
.pyproject_toml .pyproject_toml
.tool .tool
@ -854,34 +931,12 @@ impl Workspace {
return Err(WorkspaceError::NestedWorkspace(member.root.clone())); return Err(WorkspaceError::NestedWorkspace(member.root.clone()));
} }
} }
Ok(workspace_members)
let workspace_sources = workspace_pyproject_toml
.tool
.clone()
.and_then(|tool| tool.uv)
.and_then(|uv| uv.sources)
.map(ToolUvSources::into_inner)
.unwrap_or_default();
let workspace_indexes = workspace_pyproject_toml
.tool
.clone()
.and_then(|tool| tool.uv)
.and_then(|uv| uv.index)
.unwrap_or_default();
Ok(Workspace {
install_path: workspace_root,
packages: workspace_members,
sources: workspace_sources,
indexes: workspace_indexes,
pyproject_toml: workspace_pyproject_toml,
})
} }
} }
/// A project in a workspace. /// A project in a workspace.
#[derive(Debug, Clone)] #[derive(Debug, Clone, PartialEq)]
#[cfg_attr(test, derive(serde::Serialize))] #[cfg_attr(test, derive(serde::Serialize))]
pub struct WorkspaceMember { pub struct WorkspaceMember {
/// The path to the project root. /// The path to the project root.
@ -1006,7 +1061,8 @@ impl ProjectWorkspace {
/// only directories between the current path and `stop_discovery_at` are considered. /// only directories between the current path and `stop_discovery_at` are considered.
pub async fn discover( pub async fn discover(
path: &Path, path: &Path,
options: &DiscoveryOptions<'_>, options: &DiscoveryOptions,
cache: &WorkspaceCache,
) -> Result<Self, WorkspaceError> { ) -> Result<Self, WorkspaceError> {
let project_root = path let project_root = path
.ancestors() .ancestors()
@ -1014,6 +1070,7 @@ impl ProjectWorkspace {
// Only walk up the given directory, if any. // Only walk up the given directory, if any.
options options
.stop_discovery_at .stop_discovery_at
.as_deref()
.and_then(Path::parent) .and_then(Path::parent)
.map(|stop_discovery_at| stop_discovery_at != *path) .map(|stop_discovery_at| stop_discovery_at != *path)
.unwrap_or(true) .unwrap_or(true)
@ -1026,13 +1083,14 @@ impl ProjectWorkspace {
project_root.simplified_display() project_root.simplified_display()
); );
Self::from_project_root(project_root, options).await Self::from_project_root(project_root, options, cache).await
} }
/// Discover the workspace starting from the directory containing the `pyproject.toml`. /// Discover the workspace starting from the directory containing the `pyproject.toml`.
async fn from_project_root( async fn from_project_root(
project_root: &Path, project_root: &Path,
options: &DiscoveryOptions<'_>, options: &DiscoveryOptions,
cache: &WorkspaceCache,
) -> Result<Self, WorkspaceError> { ) -> Result<Self, WorkspaceError> {
// Read the current `pyproject.toml`. // Read the current `pyproject.toml`.
let pyproject_path = project_root.join("pyproject.toml"); let pyproject_path = project_root.join("pyproject.toml");
@ -1046,14 +1104,15 @@ impl ProjectWorkspace {
.clone() .clone()
.ok_or(WorkspaceError::MissingProject(pyproject_path))?; .ok_or(WorkspaceError::MissingProject(pyproject_path))?;
Self::from_project(project_root, &project, &pyproject_toml, options).await Self::from_project(project_root, &project, &pyproject_toml, options, cache).await
} }
/// If the current directory contains a `pyproject.toml` with a `project` table, discover the /// If the current directory contains a `pyproject.toml` with a `project` table, discover the
/// workspace and return it, otherwise it is a dynamic path dependency and we return `Ok(None)`. /// workspace and return it, otherwise it is a dynamic path dependency and we return `Ok(None)`.
pub async fn from_maybe_project_root( pub async fn from_maybe_project_root(
install_path: &Path, install_path: &Path,
options: &DiscoveryOptions<'_>, options: &DiscoveryOptions,
cache: &WorkspaceCache,
) -> Result<Option<Self>, WorkspaceError> { ) -> Result<Option<Self>, WorkspaceError> {
// Read the `pyproject.toml`. // Read the `pyproject.toml`.
let pyproject_path = install_path.join("pyproject.toml"); let pyproject_path = install_path.join("pyproject.toml");
@ -1070,7 +1129,7 @@ impl ProjectWorkspace {
return Ok(None); return Ok(None);
}; };
match Self::from_project(install_path, &project, &pyproject_toml, options).await { match Self::from_project(install_path, &project, &pyproject_toml, options, cache).await {
Ok(workspace) => Ok(Some(workspace)), Ok(workspace) => Ok(Some(workspace)),
Err(WorkspaceError::NonWorkspace(_)) => Ok(None), Err(WorkspaceError::NonWorkspace(_)) => Ok(None),
Err(err) => Err(err), Err(err) => Err(err),
@ -1116,7 +1175,8 @@ impl ProjectWorkspace {
install_path: &Path, install_path: &Path,
project: &Project, project: &Project,
project_pyproject_toml: &PyProjectToml, project_pyproject_toml: &PyProjectToml,
options: &DiscoveryOptions<'_>, options: &DiscoveryOptions,
cache: &WorkspaceCache,
) -> Result<Self, WorkspaceError> { ) -> Result<Self, WorkspaceError> {
let project_path = std::path::absolute(install_path) let project_path = std::path::absolute(install_path)
.map_err(WorkspaceError::Normalize)? .map_err(WorkspaceError::Normalize)?
@ -1166,8 +1226,10 @@ impl ProjectWorkspace {
// above it, so the project is an implicit workspace root identical to the project root. // above it, so the project is an implicit workspace root identical to the project root.
debug!("No workspace root found, using project root"); debug!("No workspace root found, using project root");
let current_project_as_members = let current_project_as_members = Arc::new(BTreeMap::from_iter([(
BTreeMap::from_iter([(project.name.clone(), current_project)]); project.name.clone(),
current_project,
)]));
return Ok(Self { return Ok(Self {
project_root: project_path.clone(), project_root: project_path.clone(),
project_name: project.name.clone(), project_name: project.name.clone(),
@ -1194,6 +1256,7 @@ impl ProjectWorkspace {
workspace_pyproject_toml, workspace_pyproject_toml,
Some(current_project), Some(current_project),
options, options,
cache,
) )
.await?; .await?;
@ -1208,7 +1271,7 @@ impl ProjectWorkspace {
/// Find the workspace root above the current project, if any. /// Find the workspace root above the current project, if any.
async fn find_workspace( async fn find_workspace(
project_root: &Path, project_root: &Path,
options: &DiscoveryOptions<'_>, options: &DiscoveryOptions,
) -> Result<Option<(PathBuf, ToolUvWorkspace, PyProjectToml)>, WorkspaceError> { ) -> Result<Option<(PathBuf, ToolUvWorkspace, PyProjectToml)>, WorkspaceError> {
// Skip 1 to ignore the current project itself. // Skip 1 to ignore the current project itself.
for workspace_root in project_root for workspace_root in project_root
@ -1217,6 +1280,7 @@ async fn find_workspace(
// Only walk up the given directory, if any. // Only walk up the given directory, if any.
options options
.stop_discovery_at .stop_discovery_at
.as_deref()
.and_then(Path::parent) .and_then(Path::parent)
.map(|stop_discovery_at| stop_discovery_at != *path) .map(|stop_discovery_at| stop_discovery_at != *path)
.unwrap_or(true) .unwrap_or(true)
@ -1366,7 +1430,8 @@ impl VirtualProject {
/// discovering the main workspace. /// discovering the main workspace.
pub async fn discover( pub async fn discover(
path: &Path, path: &Path,
options: &DiscoveryOptions<'_>, options: &DiscoveryOptions,
cache: &WorkspaceCache,
) -> Result<Self, WorkspaceError> { ) -> Result<Self, WorkspaceError> {
assert!( assert!(
path.is_absolute(), path.is_absolute(),
@ -1378,6 +1443,7 @@ impl VirtualProject {
// Only walk up the given directory, if any. // Only walk up the given directory, if any.
options options
.stop_discovery_at .stop_discovery_at
.as_deref()
.and_then(Path::parent) .and_then(Path::parent)
.map(|stop_discovery_at| stop_discovery_at != *path) .map(|stop_discovery_at| stop_discovery_at != *path)
.unwrap_or(true) .unwrap_or(true)
@ -1398,9 +1464,14 @@ impl VirtualProject {
if let Some(project) = pyproject_toml.project.as_ref() { if let Some(project) = pyproject_toml.project.as_ref() {
// If the `pyproject.toml` contains a `[project]` table, it's a project. // If the `pyproject.toml` contains a `[project]` table, it's a project.
let project = let project = ProjectWorkspace::from_project(
ProjectWorkspace::from_project(project_root, project, &pyproject_toml, options) project_root,
.await?; project,
&pyproject_toml,
options,
cache,
)
.await?;
Ok(Self::Project(project)) Ok(Self::Project(project))
} else if let Some(workspace) = pyproject_toml } else if let Some(workspace) = pyproject_toml
.tool .tool
@ -1420,6 +1491,7 @@ impl VirtualProject {
pyproject_toml, pyproject_toml,
None, None,
options, options,
cache,
) )
.await?; .await?;
@ -1504,7 +1576,7 @@ mod tests {
use crate::pyproject::PyProjectToml; use crate::pyproject::PyProjectToml;
use crate::workspace::{DiscoveryOptions, ProjectWorkspace}; use crate::workspace::{DiscoveryOptions, ProjectWorkspace};
use crate::WorkspaceError; use crate::{WorkspaceCache, WorkspaceError};
async fn workspace_test(folder: &str) -> (ProjectWorkspace, String) { async fn workspace_test(folder: &str) -> (ProjectWorkspace, String) {
let root_dir = env::current_dir() let root_dir = env::current_dir()
@ -1515,10 +1587,13 @@ mod tests {
.unwrap() .unwrap()
.join("scripts") .join("scripts")
.join("workspaces"); .join("workspaces");
let project = let project = ProjectWorkspace::discover(
ProjectWorkspace::discover(&root_dir.join(folder), &DiscoveryOptions::default()) &root_dir.join(folder),
.await &DiscoveryOptions::default(),
.unwrap(); &WorkspaceCache::default(),
)
.await
.unwrap();
let root_escaped = regex::escape(root_dir.to_string_lossy().as_ref()); let root_escaped = regex::escape(root_dir.to_string_lossy().as_ref());
(project, root_escaped) (project, root_escaped)
} }
@ -1527,9 +1602,13 @@ mod tests {
folder: &Path, folder: &Path,
) -> Result<(ProjectWorkspace, String), (WorkspaceError, String)> { ) -> Result<(ProjectWorkspace, String), (WorkspaceError, String)> {
let root_escaped = regex::escape(folder.to_string_lossy().as_ref()); let root_escaped = regex::escape(folder.to_string_lossy().as_ref());
let project = ProjectWorkspace::discover(folder, &DiscoveryOptions::default()) let project = ProjectWorkspace::discover(
.await folder,
.map_err(|error| (error, root_escaped.clone()))?; &DiscoveryOptions::default(),
&WorkspaceCache::default(),
)
.await
.map_err(|error| (error, root_escaped.clone()))?;
Ok((project, root_escaped)) Ok((project, root_escaped))
} }
@ -1903,6 +1982,7 @@ mod tests {
"###); "###);
}); });
} }
#[tokio::test] #[tokio::test]
async fn exclude_package() -> Result<()> { async fn exclude_package() -> Result<()> {
let root = tempfile::TempDir::new()?; let root = tempfile::TempDir::new()?;

View File

@ -43,7 +43,7 @@ use uv_requirements::RequirementsSource;
use uv_resolver::{ExcludeNewer, FlatIndex, RequiresPython}; use uv_resolver::{ExcludeNewer, FlatIndex, RequiresPython};
use uv_settings::PythonInstallMirrors; use uv_settings::PythonInstallMirrors;
use uv_types::{AnyErrorBuild, BuildContext, BuildIsolation, BuildStack, HashStrategy}; use uv_types::{AnyErrorBuild, BuildContext, BuildIsolation, BuildStack, HashStrategy};
use uv_workspace::{DiscoveryOptions, Workspace, WorkspaceError}; use uv_workspace::{DiscoveryOptions, Workspace, WorkspaceCache, WorkspaceError};
#[derive(Debug, Error)] #[derive(Debug, Error)]
enum Error { enum Error {
@ -241,7 +241,13 @@ async fn build_impl(
}; };
// Attempt to discover the workspace; on failure, save the error for later. // Attempt to discover the workspace; on failure, save the error for later.
let workspace = Workspace::discover(src.directory(), &DiscoveryOptions::default()).await; let workspace_cache = WorkspaceCache::default();
let workspace = Workspace::discover(
src.directory(),
&DiscoveryOptions::default(),
&workspace_cache,
)
.await;
// If a `--package` or `--all-packages` was provided, adjust the source directory. // If a `--package` or `--all-packages` was provided, adjust the source directory.
let packages = if let Some(package) = package { let packages = if let Some(package) = package {
@ -553,6 +559,7 @@ async fn build_package(
// Initialize any shared state. // Initialize any shared state.
let state = SharedState::default(); let state = SharedState::default();
let workspace_cache = WorkspaceCache::default();
// Create a build dispatch. // Create a build dispatch.
let build_dispatch = BuildDispatch::new( let build_dispatch = BuildDispatch::new(
@ -572,6 +579,7 @@ async fn build_package(
&hasher, &hasher,
exclude_newer, exclude_newer,
sources, sources,
workspace_cache,
concurrency, concurrency,
preview, preview,
); );

View File

@ -40,6 +40,7 @@ use uv_resolver::{
}; };
use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy}; use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy};
use uv_warnings::warn_user; use uv_warnings::warn_user;
use uv_workspace::WorkspaceCache;
use crate::commands::pip::loggers::DefaultResolveLogger; use crate::commands::pip::loggers::DefaultResolveLogger;
use crate::commands::pip::{operations, resolution_environment}; use crate::commands::pip::{operations, resolution_environment};
@ -395,6 +396,7 @@ pub(crate) async fn pip_compile(
&build_hashes, &build_hashes,
exclude_newer, exclude_newer,
sources, sources,
WorkspaceCache::default(),
concurrency, concurrency,
preview, preview,
); );

View File

@ -34,6 +34,7 @@ use uv_resolver::{
ResolutionMode, ResolverEnvironment, ResolutionMode, ResolverEnvironment,
}; };
use uv_types::{BuildIsolation, HashStrategy}; use uv_types::{BuildIsolation, HashStrategy};
use uv_workspace::WorkspaceCache;
use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger, InstallLogger}; use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger, InstallLogger};
use crate::commands::pip::operations::Modifications; use crate::commands::pip::operations::Modifications;
@ -400,6 +401,7 @@ pub(crate) async fn pip_install(
&build_hasher, &build_hasher,
exclude_newer, exclude_newer,
sources, sources,
WorkspaceCache::default(),
concurrency, concurrency,
preview, preview,
); );

View File

@ -31,6 +31,7 @@ use uv_resolver::{
ResolutionMode, ResolverEnvironment, ResolutionMode, ResolverEnvironment,
}; };
use uv_types::{BuildIsolation, HashStrategy}; use uv_types::{BuildIsolation, HashStrategy};
use uv_workspace::WorkspaceCache;
use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger}; use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger};
use crate::commands::pip::operations::Modifications; use crate::commands::pip::operations::Modifications;
@ -333,6 +334,7 @@ pub(crate) async fn pip_sync(
&build_hasher, &build_hasher,
exclude_newer, exclude_newer,
sources, sources,
WorkspaceCache::default(),
concurrency, concurrency,
preview, preview,
); );

View File

@ -37,7 +37,7 @@ use uv_types::{BuildIsolation, HashStrategy};
use uv_warnings::warn_user_once; use uv_warnings::warn_user_once;
use uv_workspace::pyproject::{DependencyType, Source, SourceError}; use uv_workspace::pyproject::{DependencyType, Source, SourceError};
use uv_workspace::pyproject_mut::{ArrayEdit, DependencyTarget, PyProjectTomlMut}; use uv_workspace::pyproject_mut::{ArrayEdit, DependencyTarget, PyProjectTomlMut};
use uv_workspace::{DiscoveryOptions, VirtualProject, Workspace}; use uv_workspace::{DiscoveryOptions, VirtualProject, Workspace, WorkspaceCache};
use crate::commands::pip::loggers::{ use crate::commands::pip::loggers::{
DefaultInstallLogger, DefaultResolveLogger, SummaryResolveLogger, DefaultInstallLogger, DefaultResolveLogger, SummaryResolveLogger,
@ -173,15 +173,25 @@ pub(crate) async fn add(
AddTarget::Script(script, Box::new(interpreter)) AddTarget::Script(script, Box::new(interpreter))
} else { } else {
// Find the project in the workspace. // Find the project in the workspace.
// No workspace caching since `uv add` changes the workspace definition.
let project = if let Some(package) = package { let project = if let Some(package) = package {
VirtualProject::Project( VirtualProject::Project(
Workspace::discover(project_dir, &DiscoveryOptions::default()) Workspace::discover(
.await? project_dir,
.with_current_project(package.clone()) &DiscoveryOptions::default(),
.with_context(|| format!("Package `{package}` not found in workspace"))?, &WorkspaceCache::default(),
)
.await?
.with_current_project(package.clone())
.with_context(|| format!("Package `{package}` not found in workspace"))?,
) )
} else { } else {
VirtualProject::discover(project_dir, &DiscoveryOptions::default()).await? VirtualProject::discover(
project_dir,
&DiscoveryOptions::default(),
&WorkspaceCache::default(),
)
.await?
}; };
// For non-project workspace roots, allow dev dependencies, but nothing else. // For non-project workspace roots, allow dev dependencies, but nothing else.
@ -340,6 +350,8 @@ pub(crate) async fn add(
&build_hasher, &build_hasher,
settings.exclude_newer, settings.exclude_newer,
sources, sources,
// No workspace caching since `uv add` changes the workspace definition.
WorkspaceCache::default(),
concurrency, concurrency,
preview, preview,
); );

View File

@ -15,7 +15,7 @@ use uv_normalize::PackageName;
use uv_python::{PythonDownloads, PythonPreference, PythonRequest}; use uv_python::{PythonDownloads, PythonPreference, PythonRequest};
use uv_resolver::RequirementsTxtExport; use uv_resolver::RequirementsTxtExport;
use uv_scripts::{Pep723ItemRef, Pep723Script}; use uv_scripts::{Pep723ItemRef, Pep723Script};
use uv_workspace::{DiscoveryOptions, MemberDiscovery, VirtualProject, Workspace}; use uv_workspace::{DiscoveryOptions, MemberDiscovery, VirtualProject, Workspace, WorkspaceCache};
use crate::commands::pip::loggers::DefaultResolveLogger; use crate::commands::pip::loggers::DefaultResolveLogger;
use crate::commands::project::install_target::InstallTarget; use crate::commands::project::install_target::InstallTarget;
@ -79,6 +79,7 @@ pub(crate) async fn export(
preview: PreviewMode, preview: PreviewMode,
) -> Result<ExitStatus> { ) -> Result<ExitStatus> {
// Identify the target. // Identify the target.
let workspace_cache = WorkspaceCache::default();
let target = if let Some(script) = script { let target = if let Some(script) = script {
ExportTarget::Script(script) ExportTarget::Script(script)
} else { } else {
@ -89,17 +90,19 @@ pub(crate) async fn export(
members: MemberDiscovery::None, members: MemberDiscovery::None,
..DiscoveryOptions::default() ..DiscoveryOptions::default()
}, },
&workspace_cache,
) )
.await? .await?
} else if let Some(package) = package.as_ref() { } else if let Some(package) = package.as_ref() {
VirtualProject::Project( VirtualProject::Project(
Workspace::discover(project_dir, &DiscoveryOptions::default()) Workspace::discover(project_dir, &DiscoveryOptions::default(), &workspace_cache)
.await? .await?
.with_current_project(package.clone()) .with_current_project(package.clone())
.with_context(|| format!("Package `{package}` not found in workspace"))?, .with_context(|| format!("Package `{package}` not found in workspace"))?,
) )
} else { } else {
VirtualProject::discover(project_dir, &DiscoveryOptions::default()).await? VirtualProject::discover(project_dir, &DiscoveryOptions::default(), &workspace_cache)
.await?
}; };
ExportTarget::Project(project) ExportTarget::Project(project)
}; };

View File

@ -26,7 +26,7 @@ use uv_scripts::{Pep723Script, ScriptTag};
use uv_settings::PythonInstallMirrors; use uv_settings::PythonInstallMirrors;
use uv_warnings::warn_user_once; use uv_warnings::warn_user_once;
use uv_workspace::pyproject_mut::{DependencyTarget, PyProjectTomlMut}; use uv_workspace::pyproject_mut::{DependencyTarget, PyProjectTomlMut};
use uv_workspace::{DiscoveryOptions, MemberDiscovery, Workspace, WorkspaceError}; use uv_workspace::{DiscoveryOptions, MemberDiscovery, Workspace, WorkspaceCache, WorkspaceError};
use crate::commands::project::{find_requires_python, init_script_python_requirement}; use crate::commands::project::{find_requires_python, init_script_python_requirement};
use crate::commands::reporters::PythonDownloadReporter; use crate::commands::reporters::PythonDownloadReporter;
@ -291,14 +291,16 @@ async fn init_project(
printer: Printer, printer: Printer,
) -> Result<()> { ) -> Result<()> {
// Discover the current workspace, if it exists. // Discover the current workspace, if it exists.
let workspace_cache = WorkspaceCache::default();
let workspace = { let workspace = {
let parent = path.parent().expect("Project path has no parent"); let parent = path.parent().expect("Project path has no parent");
match Workspace::discover( match Workspace::discover(
parent, parent,
&DiscoveryOptions { &DiscoveryOptions {
members: MemberDiscovery::Ignore(std::iter::once(path).collect()), members: MemberDiscovery::Ignore(std::iter::once(path.to_path_buf()).collect()),
..DiscoveryOptions::default() ..DiscoveryOptions::default()
}, },
&workspace_cache,
) )
.await .await
{ {

View File

@ -37,7 +37,7 @@ use uv_scripts::{Pep723ItemRef, Pep723Script};
use uv_settings::PythonInstallMirrors; use uv_settings::PythonInstallMirrors;
use uv_types::{BuildContext, BuildIsolation, EmptyInstalledPackages, HashStrategy}; use uv_types::{BuildContext, BuildIsolation, EmptyInstalledPackages, HashStrategy};
use uv_warnings::{warn_user, warn_user_once}; use uv_warnings::{warn_user, warn_user_once};
use uv_workspace::{DiscoveryOptions, Workspace, WorkspaceMember}; use uv_workspace::{DiscoveryOptions, Workspace, WorkspaceCache, WorkspaceMember};
use crate::commands::pip::loggers::{DefaultResolveLogger, ResolveLogger, SummaryResolveLogger}; use crate::commands::pip::loggers::{DefaultResolveLogger, ResolveLogger, SummaryResolveLogger};
use crate::commands::project::lock_target::LockTarget; use crate::commands::project::lock_target::LockTarget;
@ -123,11 +123,14 @@ pub(crate) async fn lock(
}; };
// Find the project requirements. // Find the project requirements.
let workspace_cache = WorkspaceCache::default();
let workspace; let workspace;
let target = if let Some(script) = script.as_ref() { let target = if let Some(script) = script.as_ref() {
LockTarget::Script(script) LockTarget::Script(script)
} else { } else {
workspace = Workspace::discover(project_dir, &DiscoveryOptions::default()).await?; workspace =
Workspace::discover(project_dir, &DiscoveryOptions::default(), &workspace_cache)
.await?;
LockTarget::Workspace(&workspace) LockTarget::Workspace(&workspace)
}; };
@ -596,6 +599,8 @@ async fn do_lock(
FlatIndex::from_entries(entries, None, &hasher, build_options) FlatIndex::from_entries(entries, None, &hasher, build_options)
}; };
let workspace_cache = WorkspaceCache::default();
// Create a build dispatch. // Create a build dispatch.
let build_dispatch = BuildDispatch::new( let build_dispatch = BuildDispatch::new(
&client, &client,
@ -614,6 +619,7 @@ async fn do_lock(
&build_hasher, &build_hasher,
exclude_newer, exclude_newer,
sources, sources,
workspace_cache,
concurrency, concurrency,
preview, preview,
); );

View File

@ -46,7 +46,7 @@ use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy};
use uv_warnings::{warn_user, warn_user_once}; use uv_warnings::{warn_user, warn_user_once};
use uv_workspace::dependency_groups::DependencyGroupError; use uv_workspace::dependency_groups::DependencyGroupError;
use uv_workspace::pyproject::PyProjectToml; use uv_workspace::pyproject::PyProjectToml;
use uv_workspace::Workspace; use uv_workspace::{Workspace, WorkspaceCache};
use crate::commands::pip::loggers::{InstallLogger, ResolveLogger}; use crate::commands::pip::loggers::{InstallLogger, ResolveLogger};
use crate::commands::pip::operations::{Changelog, Modifications}; use crate::commands::pip::operations::{Changelog, Modifications};
@ -1482,6 +1482,7 @@ pub(crate) async fn resolve_names(
state: &SharedState, state: &SharedState,
concurrency: Concurrency, concurrency: Concurrency,
cache: &Cache, cache: &Cache,
workspace_cache: &WorkspaceCache,
printer: Printer, printer: Printer,
preview: PreviewMode, preview: PreviewMode,
) -> Result<Vec<Requirement>, uv_requirements::Error> { ) -> Result<Vec<Requirement>, uv_requirements::Error> {
@ -1583,6 +1584,7 @@ pub(crate) async fn resolve_names(
&build_hasher, &build_hasher,
*exclude_newer, *exclude_newer,
*sources, *sources,
workspace_cache.clone(),
concurrency, concurrency,
preview, preview,
); );
@ -1754,6 +1756,8 @@ pub(crate) async fn resolve_environment(
FlatIndex::from_entries(entries, Some(tags), &hasher, build_options) FlatIndex::from_entries(entries, Some(tags), &hasher, build_options)
}; };
let workspace_cache = WorkspaceCache::default();
// Create a build dispatch. // Create a build dispatch.
let resolve_dispatch = BuildDispatch::new( let resolve_dispatch = BuildDispatch::new(
&client, &client,
@ -1772,6 +1776,7 @@ pub(crate) async fn resolve_environment(
&build_hasher, &build_hasher,
exclude_newer, exclude_newer,
sources, sources,
workspace_cache,
concurrency, concurrency,
preview, preview,
); );
@ -1883,6 +1888,7 @@ pub(crate) async fn sync_environment(
let build_hasher = HashStrategy::default(); let build_hasher = HashStrategy::default();
let dry_run = DryRun::default(); let dry_run = DryRun::default();
let hasher = HashStrategy::default(); let hasher = HashStrategy::default();
let workspace_cache = WorkspaceCache::default();
// Resolve the flat indexes from `--find-links`. // Resolve the flat indexes from `--find-links`.
let flat_index = { let flat_index = {
@ -1911,6 +1917,7 @@ pub(crate) async fn sync_environment(
&build_hasher, &build_hasher,
exclude_newer, exclude_newer,
sources, sources,
workspace_cache,
concurrency, concurrency,
preview, preview,
); );
@ -1976,6 +1983,7 @@ pub(crate) async fn update_environment(
installer_metadata: bool, installer_metadata: bool,
concurrency: Concurrency, concurrency: Concurrency,
cache: &Cache, cache: &Cache,
workspace_cache: WorkspaceCache,
dry_run: DryRun, dry_run: DryRun,
printer: Printer, printer: Printer,
preview: PreviewMode, preview: PreviewMode,
@ -2129,6 +2137,7 @@ pub(crate) async fn update_environment(
&build_hasher, &build_hasher,
*exclude_newer, *exclude_newer,
*sources, *sources,
workspace_cache,
concurrency, concurrency,
preview, preview,
); );

View File

@ -21,7 +21,7 @@ use uv_settings::PythonInstallMirrors;
use uv_warnings::warn_user_once; use uv_warnings::warn_user_once;
use uv_workspace::pyproject::DependencyType; use uv_workspace::pyproject::DependencyType;
use uv_workspace::pyproject_mut::{DependencyTarget, PyProjectTomlMut}; use uv_workspace::pyproject_mut::{DependencyTarget, PyProjectTomlMut};
use uv_workspace::{DiscoveryOptions, VirtualProject, Workspace}; use uv_workspace::{DiscoveryOptions, VirtualProject, Workspace, WorkspaceCache};
use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger}; use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger};
use crate::commands::pip::operations::Modifications; use crate::commands::pip::operations::Modifications;
@ -87,15 +87,25 @@ pub(crate) async fn remove(
RemoveTarget::Script(script) RemoveTarget::Script(script)
} else { } else {
// Find the project in the workspace. // Find the project in the workspace.
// No workspace caching since `uv remove` changes the workspace definition.
let project = if let Some(package) = package { let project = if let Some(package) = package {
VirtualProject::Project( VirtualProject::Project(
Workspace::discover(project_dir, &DiscoveryOptions::default()) Workspace::discover(
.await? project_dir,
.with_current_project(package.clone()) &DiscoveryOptions::default(),
.with_context(|| format!("Package `{package}` not found in workspace"))?, &WorkspaceCache::default(),
)
.await?
.with_current_project(package.clone())
.with_context(|| format!("Package `{package}` not found in workspace"))?,
) )
} else { } else {
VirtualProject::discover(project_dir, &DiscoveryOptions::default()).await? VirtualProject::discover(
project_dir,
&DiscoveryOptions::default(),
&WorkspaceCache::default(),
)
.await?
}; };
RemoveTarget::Project(project) RemoveTarget::Project(project)

View File

@ -35,7 +35,7 @@ use uv_scripts::Pep723Item;
use uv_settings::PythonInstallMirrors; use uv_settings::PythonInstallMirrors;
use uv_static::EnvVars; use uv_static::EnvVars;
use uv_warnings::warn_user; use uv_warnings::warn_user;
use uv_workspace::{DiscoveryOptions, VirtualProject, Workspace, WorkspaceError}; use uv_workspace::{DiscoveryOptions, VirtualProject, Workspace, WorkspaceCache, WorkspaceError};
use crate::commands::pip::loggers::{ use crate::commands::pip::loggers::{
DefaultInstallLogger, DefaultResolveLogger, SummaryInstallLogger, SummaryResolveLogger, DefaultInstallLogger, DefaultResolveLogger, SummaryInstallLogger, SummaryResolveLogger,
@ -142,6 +142,7 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl
// Initialize any shared state. // Initialize any shared state.
let lock_state = UniversalState::default(); let lock_state = UniversalState::default();
let sync_state = lock_state.fork(); let sync_state = lock_state.fork();
let workspace_cache = WorkspaceCache::default();
// Read from the `.env` file, if necessary. // Read from the `.env` file, if necessary.
if !no_env_file { if !no_env_file {
@ -369,6 +370,7 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl
installer_metadata, installer_metadata,
concurrency, concurrency,
cache, cache,
workspace_cache,
DryRun::Disabled, DryRun::Disabled,
printer, printer,
preview, preview,
@ -425,6 +427,7 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl
let mut lock: Option<(Lock, PathBuf)> = None; let mut lock: Option<(Lock, PathBuf)> = None;
// Discover and sync the base environment. // Discover and sync the base environment.
let workspace_cache = WorkspaceCache::default();
let temp_dir; let temp_dir;
let base_interpreter = if let Some(script_interpreter) = script_interpreter { let base_interpreter = if let Some(script_interpreter) = script_interpreter {
// If we found a PEP 723 script and the user provided a project-only setting, warn. // If we found a PEP 723 script and the user provided a project-only setting, warn.
@ -466,13 +469,19 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl
// We need a workspace, but we don't need to have a current package, we can be e.g. in // We need a workspace, but we don't need to have a current package, we can be e.g. in
// the root of a virtual workspace and then switch into the selected package. // the root of a virtual workspace and then switch into the selected package.
Some(VirtualProject::Project( Some(VirtualProject::Project(
Workspace::discover(project_dir, &DiscoveryOptions::default()) Workspace::discover(project_dir, &DiscoveryOptions::default(), &workspace_cache)
.await? .await?
.with_current_project(package.clone()) .with_current_project(package.clone())
.with_context(|| format!("Package `{package}` not found in workspace"))?, .with_context(|| format!("Package `{package}` not found in workspace"))?,
)) ))
} else { } else {
match VirtualProject::discover(project_dir, &DiscoveryOptions::default()).await { match VirtualProject::discover(
project_dir,
&DiscoveryOptions::default(),
&workspace_cache,
)
.await
{
Ok(project) => { Ok(project) => {
if no_project { if no_project {
debug!("Ignoring discovered project due to `--no-project`"); debug!("Ignoring discovered project due to `--no-project`");

View File

@ -30,7 +30,7 @@ use uv_settings::PythonInstallMirrors;
use uv_types::{BuildIsolation, HashStrategy}; use uv_types::{BuildIsolation, HashStrategy};
use uv_warnings::warn_user; use uv_warnings::warn_user;
use uv_workspace::pyproject::Source; use uv_workspace::pyproject::Source;
use uv_workspace::{DiscoveryOptions, MemberDiscovery, VirtualProject, Workspace}; use uv_workspace::{DiscoveryOptions, MemberDiscovery, VirtualProject, Workspace, WorkspaceCache};
use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger, InstallLogger}; use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger, InstallLogger};
use crate::commands::pip::operations; use crate::commands::pip::operations;
@ -76,6 +76,7 @@ pub(crate) async fn sync(
preview: PreviewMode, preview: PreviewMode,
) -> Result<ExitStatus> { ) -> Result<ExitStatus> {
// Identify the target. // Identify the target.
let workspace_cache = WorkspaceCache::default();
let target = if let Some(script) = script { let target = if let Some(script) = script {
SyncTarget::Script(script) SyncTarget::Script(script)
} else { } else {
@ -87,17 +88,19 @@ pub(crate) async fn sync(
members: MemberDiscovery::None, members: MemberDiscovery::None,
..DiscoveryOptions::default() ..DiscoveryOptions::default()
}, },
&workspace_cache,
) )
.await? .await?
} else if let Some(package) = package.as_ref() { } else if let Some(package) = package.as_ref() {
VirtualProject::Project( VirtualProject::Project(
Workspace::discover(project_dir, &DiscoveryOptions::default()) Workspace::discover(project_dir, &DiscoveryOptions::default(), &workspace_cache)
.await? .await?
.with_current_project(package.clone()) .with_current_project(package.clone())
.with_context(|| format!("Package `{package}` not found in workspace"))?, .with_context(|| format!("Package `{package}` not found in workspace"))?,
) )
} else { } else {
VirtualProject::discover(project_dir, &DiscoveryOptions::default()).await? VirtualProject::discover(project_dir, &DiscoveryOptions::default(), &workspace_cache)
.await?
}; };
// TODO(lucab): improve warning content // TODO(lucab): improve warning content
@ -286,6 +289,7 @@ pub(crate) async fn sync(
installer_metadata, installer_metadata,
concurrency, concurrency,
cache, cache,
workspace_cache,
dry_run, dry_run,
printer, printer,
preview, preview,
@ -676,6 +680,7 @@ pub(super) async fn do_sync(
&build_hasher, &build_hasher,
exclude_newer, exclude_newer,
sources, sources,
WorkspaceCache::default(),
concurrency, concurrency,
preview, preview,
); );

View File

@ -15,7 +15,7 @@ use uv_python::{PythonDownloads, PythonPreference, PythonRequest, PythonVersion}
use uv_resolver::{PackageMap, TreeDisplay}; use uv_resolver::{PackageMap, TreeDisplay};
use uv_scripts::{Pep723ItemRef, Pep723Script}; use uv_scripts::{Pep723ItemRef, Pep723Script};
use uv_settings::PythonInstallMirrors; use uv_settings::PythonInstallMirrors;
use uv_workspace::{DiscoveryOptions, Workspace}; use uv_workspace::{DiscoveryOptions, Workspace, WorkspaceCache};
use crate::commands::pip::latest::LatestClient; use crate::commands::pip::latest::LatestClient;
use crate::commands::pip::loggers::DefaultResolveLogger; use crate::commands::pip::loggers::DefaultResolveLogger;
@ -60,11 +60,14 @@ pub(crate) async fn tree(
preview: PreviewMode, preview: PreviewMode,
) -> Result<ExitStatus> { ) -> Result<ExitStatus> {
// Find the project requirements. // Find the project requirements.
let workspace_cache = WorkspaceCache::default();
let workspace; let workspace;
let target = if let Some(script) = script.as_ref() { let target = if let Some(script) = script.as_ref() {
LockTarget::Script(script) LockTarget::Script(script)
} else { } else {
workspace = Workspace::discover(project_dir, &DiscoveryOptions::default()).await?; workspace =
Workspace::discover(project_dir, &DiscoveryOptions::default(), &workspace_cache)
.await?;
LockTarget::Workspace(&workspace) LockTarget::Workspace(&workspace)
}; };

View File

@ -6,7 +6,7 @@ use uv_cache::Cache;
use uv_fs::Simplified; use uv_fs::Simplified;
use uv_python::{EnvironmentPreference, PythonInstallation, PythonPreference, PythonRequest}; use uv_python::{EnvironmentPreference, PythonInstallation, PythonPreference, PythonRequest};
use uv_warnings::{warn_user, warn_user_once}; use uv_warnings::{warn_user, warn_user_once};
use uv_workspace::{DiscoveryOptions, VirtualProject, WorkspaceError}; use uv_workspace::{DiscoveryOptions, VirtualProject, WorkspaceCache, WorkspaceError};
use crate::commands::{ use crate::commands::{
project::{validate_project_requires_python, WorkspacePython}, project::{validate_project_requires_python, WorkspacePython},
@ -29,10 +29,13 @@ pub(crate) async fn find(
EnvironmentPreference::Any EnvironmentPreference::Any
}; };
let workspace_cache = WorkspaceCache::default();
let project = if no_project { let project = if no_project {
None None
} else { } else {
match VirtualProject::discover(project_dir, &DiscoveryOptions::default()).await { match VirtualProject::discover(project_dir, &DiscoveryOptions::default(), &workspace_cache)
.await
{
Ok(project) => Some(project), Ok(project) => Some(project),
Err(WorkspaceError::MissingProject(_)) => None, Err(WorkspaceError::MissingProject(_)) => None,
Err(WorkspaceError::MissingPyprojectToml) => None, Err(WorkspaceError::MissingPyprojectToml) => None,

View File

@ -13,7 +13,7 @@ use uv_python::{
VersionFileDiscoveryOptions, PYTHON_VERSION_FILENAME, VersionFileDiscoveryOptions, PYTHON_VERSION_FILENAME,
}; };
use uv_warnings::warn_user_once; use uv_warnings::warn_user_once;
use uv_workspace::{DiscoveryOptions, VirtualProject}; use uv_workspace::{DiscoveryOptions, VirtualProject, WorkspaceCache};
use crate::commands::{project::find_requires_python, ExitStatus}; use crate::commands::{project::find_requires_python, ExitStatus};
use crate::printer::Printer; use crate::printer::Printer;
@ -28,10 +28,13 @@ pub(crate) async fn pin(
cache: &Cache, cache: &Cache,
printer: Printer, printer: Printer,
) -> Result<ExitStatus> { ) -> Result<ExitStatus> {
let workspace_cache = WorkspaceCache::default();
let virtual_project = if no_project { let virtual_project = if no_project {
None None
} else { } else {
match VirtualProject::discover(project_dir, &DiscoveryOptions::default()).await { match VirtualProject::discover(project_dir, &DiscoveryOptions::default(), &workspace_cache)
.await
{
Ok(virtual_project) => Some(virtual_project), Ok(virtual_project) => Some(virtual_project),
Err(err) => { Err(err) => {
debug!("Failed to discover virtual project: {err}"); debug!("Failed to discover virtual project: {err}");

View File

@ -21,9 +21,9 @@ use uv_requirements::{RequirementsSource, RequirementsSpecification};
use uv_settings::{PythonInstallMirrors, ResolverInstallerOptions, ToolOptions}; use uv_settings::{PythonInstallMirrors, ResolverInstallerOptions, ToolOptions};
use uv_tool::InstalledTools; use uv_tool::InstalledTools;
use uv_warnings::warn_user; use uv_warnings::warn_user;
use uv_workspace::WorkspaceCache;
use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger}; use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger};
use crate::commands::pip::operations::Modifications; use crate::commands::pip::operations::Modifications;
use crate::commands::project::{ use crate::commands::project::{
resolve_environment, resolve_names, sync_environment, update_environment, resolve_environment, resolve_names, sync_environment, update_environment,
@ -86,6 +86,7 @@ pub(crate) async fn install(
// Initialize any shared state. // Initialize any shared state.
let state = PlatformState::default(); let state = PlatformState::default();
let workspace_cache = WorkspaceCache::default();
let client_builder = BaseClientBuilder::new() let client_builder = BaseClientBuilder::new()
.connectivity(network_settings.connectivity) .connectivity(network_settings.connectivity)
@ -133,6 +134,7 @@ pub(crate) async fn install(
&state, &state,
concurrency, concurrency,
&cache, &cache,
&workspace_cache,
printer, printer,
preview, preview,
) )
@ -245,6 +247,7 @@ pub(crate) async fn install(
&state, &state,
concurrency, concurrency,
&cache, &cache,
&workspace_cache,
printer, printer,
preview, preview,
) )
@ -269,6 +272,7 @@ pub(crate) async fn install(
&state, &state,
concurrency, concurrency,
&cache, &cache,
&workspace_cache,
printer, printer,
preview, preview,
) )
@ -400,6 +404,7 @@ pub(crate) async fn install(
installer_metadata, installer_metadata,
concurrency, concurrency,
&cache, &cache,
workspace_cache,
DryRun::Disabled, DryRun::Disabled,
printer, printer,
preview, preview,

View File

@ -37,6 +37,7 @@ use uv_settings::{PythonInstallMirrors, ResolverInstallerOptions, ToolOptions};
use uv_static::EnvVars; use uv_static::EnvVars;
use uv_tool::{entrypoint_paths, InstalledTools}; use uv_tool::{entrypoint_paths, InstalledTools};
use uv_warnings::warn_user; use uv_warnings::warn_user;
use uv_workspace::WorkspaceCache;
use crate::commands::pip::loggers::{ use crate::commands::pip::loggers::{
DefaultInstallLogger, DefaultResolveLogger, SummaryInstallLogger, SummaryResolveLogger, DefaultInstallLogger, DefaultResolveLogger, SummaryInstallLogger, SummaryResolveLogger,
@ -625,6 +626,7 @@ async fn get_or_create_environment(
// Initialize any shared state. // Initialize any shared state.
let state = PlatformState::default(); let state = PlatformState::default();
let workspace_cache = WorkspaceCache::default();
let from = if request.is_python() { let from = if request.is_python() {
ToolRequirement::Python ToolRequirement::Python
@ -668,6 +670,7 @@ async fn get_or_create_environment(
&state, &state,
concurrency, concurrency,
cache, cache,
&workspace_cache,
printer, printer,
preview, preview,
) )
@ -760,6 +763,7 @@ async fn get_or_create_environment(
&state, &state,
concurrency, concurrency,
cache, cache,
&workspace_cache,
printer, printer,
preview, preview,
) )
@ -785,6 +789,7 @@ async fn get_or_create_environment(
&state, &state,
concurrency, concurrency,
cache, cache,
&workspace_cache,
printer, printer,
preview, preview,
) )

View File

@ -18,6 +18,7 @@ use uv_python::{
use uv_requirements::RequirementsSpecification; use uv_requirements::RequirementsSpecification;
use uv_settings::{Combine, PythonInstallMirrors, ResolverInstallerOptions, ToolOptions}; use uv_settings::{Combine, PythonInstallMirrors, ResolverInstallerOptions, ToolOptions};
use uv_tool::InstalledTools; use uv_tool::InstalledTools;
use uv_workspace::WorkspaceCache;
use crate::commands::pip::loggers::{ use crate::commands::pip::loggers::{
DefaultInstallLogger, SummaryResolveLogger, UpgradeInstallLogger, DefaultInstallLogger, SummaryResolveLogger, UpgradeInstallLogger,
@ -281,6 +282,7 @@ async fn upgrade_tool(
// Initialize any shared state. // Initialize any shared state.
let state = PlatformState::default(); let state = PlatformState::default();
let workspace_cache = WorkspaceCache::default();
// Check if we need to create a new environment — if so, resolve it first, then // Check if we need to create a new environment — if so, resolve it first, then
// install the requested tool // install the requested tool
@ -340,6 +342,7 @@ async fn upgrade_tool(
installer_metadata, installer_metadata,
concurrency, concurrency,
cache, cache,
workspace_cache,
DryRun::Disabled, DryRun::Disabled,
printer, printer,
preview, preview,

View File

@ -29,7 +29,7 @@ use uv_settings::PythonInstallMirrors;
use uv_shell::{shlex_posix, shlex_windows, Shell}; use uv_shell::{shlex_posix, shlex_windows, Shell};
use uv_types::{AnyErrorBuild, BuildContext, BuildIsolation, BuildStack, HashStrategy}; use uv_types::{AnyErrorBuild, BuildContext, BuildIsolation, BuildStack, HashStrategy};
use uv_warnings::warn_user; use uv_warnings::warn_user;
use uv_workspace::{DiscoveryOptions, VirtualProject, WorkspaceError}; use uv_workspace::{DiscoveryOptions, VirtualProject, WorkspaceCache, WorkspaceError};
use crate::commands::pip::loggers::{DefaultInstallLogger, InstallLogger}; use crate::commands::pip::loggers::{DefaultInstallLogger, InstallLogger};
use crate::commands::pip::operations::{report_interpreter, Changelog}; use crate::commands::pip::operations::{report_interpreter, Changelog};
@ -150,10 +150,13 @@ async fn venv_impl(
relocatable: bool, relocatable: bool,
preview: PreviewMode, preview: PreviewMode,
) -> miette::Result<ExitStatus> { ) -> miette::Result<ExitStatus> {
let workspace_cache = WorkspaceCache::default();
let project = if no_project { let project = if no_project {
None None
} else { } else {
match VirtualProject::discover(project_dir, &DiscoveryOptions::default()).await { match VirtualProject::discover(project_dir, &DiscoveryOptions::default(), &workspace_cache)
.await
{
Ok(project) => Some(project), Ok(project) => Some(project),
Err(WorkspaceError::MissingProject(_)) => None, Err(WorkspaceError::MissingProject(_)) => None,
Err(WorkspaceError::MissingPyprojectToml) => None, Err(WorkspaceError::MissingPyprojectToml) => None,
@ -318,6 +321,7 @@ async fn venv_impl(
// Initialize any shared state. // Initialize any shared state.
let state = SharedState::default(); let state = SharedState::default();
let workspace_cache = WorkspaceCache::default();
// For seed packages, assume a bunch of default settings are sufficient. // For seed packages, assume a bunch of default settings are sufficient.
let build_constraints = Constraints::default(); let build_constraints = Constraints::default();
@ -346,6 +350,7 @@ async fn venv_impl(
&build_hasher, &build_hasher,
exclude_newer, exclude_newer,
sources, sources,
workspace_cache,
concurrency, concurrency,
preview, preview,
); );

View File

@ -31,7 +31,7 @@ use uv_scripts::{Pep723Error, Pep723Item, Pep723Metadata, Pep723Script};
use uv_settings::{Combine, FilesystemOptions, Options}; use uv_settings::{Combine, FilesystemOptions, Options};
use uv_static::EnvVars; use uv_static::EnvVars;
use uv_warnings::{warn_user, warn_user_once}; use uv_warnings::{warn_user, warn_user_once};
use uv_workspace::{DiscoveryOptions, Workspace}; use uv_workspace::{DiscoveryOptions, Workspace, WorkspaceCache};
use crate::commands::{ExitStatus, RunCommand, ScriptPath, ToolRunCommand}; use crate::commands::{ExitStatus, RunCommand, ScriptPath, ToolRunCommand};
use crate::printer::Printer; use crate::printer::Printer;
@ -109,6 +109,7 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
// If found, this file is combined with the user configuration file. // If found, this file is combined with the user configuration file.
// 3. The nearest configuration file (`uv.toml` or `pyproject.toml`) in the directory tree, // 3. The nearest configuration file (`uv.toml` or `pyproject.toml`) in the directory tree,
// starting from the current directory. // starting from the current directory.
let workspace_cache = WorkspaceCache::default();
let filesystem = if let Some(config_file) = cli.top_level.config_file.as_ref() { let filesystem = if let Some(config_file) = cli.top_level.config_file.as_ref() {
if config_file if config_file
.file_name() .file_name()
@ -123,7 +124,7 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
// For commands that operate at the user-level, ignore local configuration. // For commands that operate at the user-level, ignore local configuration.
FilesystemOptions::user()?.combine(FilesystemOptions::system()?) FilesystemOptions::user()?.combine(FilesystemOptions::system()?)
} else if let Ok(workspace) = } else if let Ok(workspace) =
Workspace::discover(&project_dir, &DiscoveryOptions::default()).await Workspace::discover(&project_dir, &DiscoveryOptions::default(), &workspace_cache).await
{ {
let project = FilesystemOptions::find(workspace.install_path())?; let project = FilesystemOptions::find(workspace.install_path())?;
let system = FilesystemOptions::system()?; let system = FilesystemOptions::system()?;

View File

@ -15194,7 +15194,7 @@ fn lock_explicit_default_index() -> Result<()> {
"#, "#,
)?; )?;
uv_snapshot!(context.filters(), context.lock().arg("--verbose"), @r###" uv_snapshot!(context.filters(), context.lock().arg("--verbose"), @r#"
success: false success: false
exit_code: 1 exit_code: 1
----- stdout ----- ----- stdout -----
@ -15202,7 +15202,7 @@ fn lock_explicit_default_index() -> Result<()> {
----- stderr ----- ----- stderr -----
DEBUG uv [VERSION] ([COMMIT] DATE) DEBUG uv [VERSION] ([COMMIT] DATE)
DEBUG Found workspace root: `[TEMP_DIR]/` DEBUG Found workspace root: `[TEMP_DIR]/`
DEBUG Adding current workspace member: `[TEMP_DIR]/` DEBUG Adding root workspace member: `[TEMP_DIR]/`
DEBUG Using Python request `>=3.12` from `requires-python` metadata DEBUG Using Python request `>=3.12` from `requires-python` metadata
DEBUG Checking for Python environment at `.venv` DEBUG Checking for Python environment at `.venv`
DEBUG The virtual environment's Python version satisfies `>=3.12` DEBUG The virtual environment's Python version satisfies `>=3.12`
@ -15226,7 +15226,7 @@ fn lock_explicit_default_index() -> Result<()> {
Because anyio was not found in the provided package locations and your project depends on anyio, we can conclude that your project's requirements are unsatisfiable. Because anyio was not found in the provided package locations and your project depends on anyio, we can conclude that your project's requirements are unsatisfiable.
hint: Packages were unavailable because index lookups were disabled and no additional package locations were provided (try: `--find-links <uri>`) hint: Packages were unavailable because index lookups were disabled and no additional package locations were provided (try: `--find-links <uri>`)
"###); "#);
let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap(); let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap();