mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-04 10:58:28 +00:00
Allow uv sync --package
without copying member pyproject.toml
(#6943)
## Summary Closes https://github.com/astral-sh/uv/issues/6935.
This commit is contained in:
parent
6897001fee
commit
f9c04581e6
8 changed files with 145 additions and 59 deletions
|
@ -35,7 +35,7 @@ use uv_fs::{relative_to, PortablePath, PortablePathBuf};
|
|||
use uv_git::{GitReference, GitSha, RepositoryReference, ResolvedRepositoryReference};
|
||||
use uv_normalize::{ExtraName, GroupName, PackageName};
|
||||
use uv_types::BuildContext;
|
||||
use uv_workspace::{VirtualProject, Workspace};
|
||||
use uv_workspace::{InstallTarget, Workspace};
|
||||
|
||||
pub use crate::lock::requirements_txt::RequirementsTxtExport;
|
||||
pub use crate::lock::tree::TreeDisplay;
|
||||
|
@ -426,7 +426,7 @@ impl Lock {
|
|||
/// Convert the [`Lock`] to a [`Resolution`] using the given marker environment, tags, and root.
|
||||
pub fn to_resolution(
|
||||
&self,
|
||||
project: &VirtualProject,
|
||||
project: InstallTarget<'_>,
|
||||
marker_env: &ResolverMarkerEnvironment,
|
||||
tags: &Tags,
|
||||
extras: &ExtrasSpecification,
|
||||
|
@ -439,8 +439,12 @@ impl Lock {
|
|||
for root_name in project.packages() {
|
||||
let root = self
|
||||
.find_by_name(root_name)
|
||||
.expect("found too many packages matching root")
|
||||
.expect("could not find root");
|
||||
.map_err(|_| LockErrorKind::MultipleRootPackages {
|
||||
name: root_name.clone(),
|
||||
})?
|
||||
.ok_or_else(|| LockErrorKind::MissingRootPackage {
|
||||
name: root_name.clone(),
|
||||
})?;
|
||||
|
||||
// Add the base package.
|
||||
queue.push_back((root, None));
|
||||
|
@ -466,10 +470,15 @@ impl Lock {
|
|||
for group in dev {
|
||||
for dependency in project.group(group) {
|
||||
if dependency.marker.evaluate(marker_env, &[]) {
|
||||
let root_name = &dependency.name;
|
||||
let root = self
|
||||
.find_by_markers(&dependency.name, marker_env)
|
||||
.expect("found too many packages matching root")
|
||||
.expect("could not find root");
|
||||
.find_by_markers(root_name, marker_env)
|
||||
.map_err(|_| LockErrorKind::MultipleRootPackages {
|
||||
name: root_name.clone(),
|
||||
})?
|
||||
.ok_or_else(|| LockErrorKind::MissingRootPackage {
|
||||
name: root_name.clone(),
|
||||
})?;
|
||||
|
||||
// Add the base package.
|
||||
queue.push_back((root, None));
|
||||
|
@ -3605,6 +3614,19 @@ enum LockErrorKind {
|
|||
/// An error that occurs when converting a URL to a path
|
||||
#[error("failed to convert URL to path")]
|
||||
UrlToPath,
|
||||
/// An error that occurs when multiple packages with the same
|
||||
/// name were found when identifying the root packages.
|
||||
#[error("found multiple packages matching `{name}`")]
|
||||
MultipleRootPackages {
|
||||
/// The ID of the package.
|
||||
name: PackageName,
|
||||
},
|
||||
/// An error that occurs when a root package can't be found.
|
||||
#[error("could not find root package `{name}`")]
|
||||
MissingRootPackage {
|
||||
/// The ID of the package.
|
||||
name: PackageName,
|
||||
},
|
||||
}
|
||||
|
||||
/// An error that occurs when a source string could not be parsed.
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
pub use workspace::{
|
||||
check_nested_workspaces, DiscoveryOptions, MemberDiscovery, ProjectWorkspace, VirtualProject,
|
||||
Workspace, WorkspaceError, WorkspaceMember,
|
||||
check_nested_workspaces, DiscoveryOptions, InstallTarget, MemberDiscovery, ProjectWorkspace,
|
||||
VirtualProject, Workspace, WorkspaceError, WorkspaceMember,
|
||||
};
|
||||
|
||||
pub mod pyproject;
|
||||
|
|
|
@ -1219,7 +1219,7 @@ fn is_included_in_workspace(
|
|||
Ok(false)
|
||||
}
|
||||
|
||||
/// A project that can be synced.
|
||||
/// A project that can be discovered.
|
||||
///
|
||||
/// The project could be a package within a workspace, a real workspace root, or a (legacy)
|
||||
/// non-project workspace root, which can define its own dev dependencies.
|
||||
|
@ -1311,13 +1311,13 @@ impl VirtualProject {
|
|||
#[must_use]
|
||||
pub fn with_pyproject_toml(self, pyproject_toml: PyProjectToml) -> Option<Self> {
|
||||
match self {
|
||||
VirtualProject::Project(project) => Some(VirtualProject::Project(
|
||||
project.with_pyproject_toml(pyproject_toml)?,
|
||||
)),
|
||||
VirtualProject::NonProject(workspace) => {
|
||||
Self::Project(project) => {
|
||||
Some(Self::Project(project.with_pyproject_toml(pyproject_toml)?))
|
||||
}
|
||||
Self::NonProject(workspace) => {
|
||||
// If this is a non-project workspace root, then by definition the root isn't a
|
||||
// member, so we can just update the top-level `pyproject.toml`.
|
||||
Some(VirtualProject::NonProject(Workspace {
|
||||
Some(Self::NonProject(Workspace {
|
||||
pyproject_toml,
|
||||
..workspace.clone()
|
||||
}))
|
||||
|
@ -1328,38 +1328,77 @@ impl VirtualProject {
|
|||
/// Return the root of the project.
|
||||
pub fn root(&self) -> &Path {
|
||||
match self {
|
||||
VirtualProject::Project(project) => project.project_root(),
|
||||
VirtualProject::NonProject(workspace) => workspace.install_path(),
|
||||
Self::Project(project) => project.project_root(),
|
||||
Self::NonProject(workspace) => workspace.install_path(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the [`PyProjectToml`] of the project.
|
||||
pub fn pyproject_toml(&self) -> &PyProjectToml {
|
||||
match self {
|
||||
VirtualProject::Project(project) => project.current_project().pyproject_toml(),
|
||||
VirtualProject::NonProject(workspace) => &workspace.pyproject_toml,
|
||||
Self::Project(project) => project.current_project().pyproject_toml(),
|
||||
Self::NonProject(workspace) => &workspace.pyproject_toml,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the [`Workspace`] of the project.
|
||||
pub fn workspace(&self) -> &Workspace {
|
||||
match self {
|
||||
VirtualProject::Project(project) => project.workspace(),
|
||||
VirtualProject::NonProject(workspace) => workspace,
|
||||
Self::Project(project) => project.workspace(),
|
||||
Self::NonProject(workspace) => workspace,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the [`PackageName`] of the project.
|
||||
/// Return the [`PackageName`] of the project, if available.
|
||||
pub fn project_name(&self) -> Option<&PackageName> {
|
||||
match self {
|
||||
VirtualProject::Project(project) => Some(project.project_name()),
|
||||
VirtualProject::NonProject(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the project is a virtual workspace root.
|
||||
pub fn is_non_project(&self) -> bool {
|
||||
matches!(self, VirtualProject::NonProject(_))
|
||||
}
|
||||
}
|
||||
|
||||
/// A target that can be installed.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum InstallTarget<'env> {
|
||||
/// A project (which could be a workspace root or member).
|
||||
Project(&'env ProjectWorkspace),
|
||||
/// A (legacy) non-project workspace root.
|
||||
NonProject(&'env Workspace),
|
||||
/// A frozen member within a [`Workspace`].
|
||||
FrozenMember(&'env Workspace, &'env PackageName),
|
||||
}
|
||||
|
||||
impl<'env> InstallTarget<'env> {
|
||||
/// Create an [`InstallTarget`] for a frozen member within a workspace.
|
||||
pub fn frozen_member(project: &'env VirtualProject, package_name: &'env PackageName) -> Self {
|
||||
Self::FrozenMember(project.workspace(), package_name)
|
||||
}
|
||||
|
||||
/// Return the [`Workspace`] of the target.
|
||||
pub fn workspace(&self) -> &Workspace {
|
||||
match self {
|
||||
Self::Project(project) => project.workspace(),
|
||||
Self::NonProject(workspace) => workspace,
|
||||
Self::FrozenMember(workspace, _) => workspace,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the [`PackageName`] of the target.
|
||||
pub fn packages(&self) -> impl Iterator<Item = &PackageName> {
|
||||
match self {
|
||||
VirtualProject::Project(project) => {
|
||||
Either::Left(std::iter::once(project.project_name()))
|
||||
}
|
||||
VirtualProject::NonProject(workspace) => Either::Right(workspace.packages().keys()),
|
||||
Self::Project(project) => Either::Left(std::iter::once(project.project_name())),
|
||||
Self::NonProject(workspace) => Either::Right(workspace.packages().keys()),
|
||||
Self::FrozenMember(_, package_name) => Either::Left(std::iter::once(*package_name)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the [`VirtualProject`] dependencies for the given group name.
|
||||
/// Return the [`InstallTarget`] dependencies for the given group name.
|
||||
///
|
||||
/// Returns dependencies that apply to the workspace root, but not any of its members. As such,
|
||||
/// only returns a non-empty iterator for virtual workspaces, which can include dev dependencies
|
||||
|
@ -1369,11 +1408,11 @@ impl VirtualProject {
|
|||
name: &GroupName,
|
||||
) -> impl Iterator<Item = &pep508_rs::Requirement<VerbatimParsedUrl>> {
|
||||
match self {
|
||||
VirtualProject::Project(_) => {
|
||||
Self::Project(_) | Self::FrozenMember(..) => {
|
||||
// For projects, dev dependencies are attached to the members.
|
||||
Either::Left(std::iter::empty())
|
||||
}
|
||||
VirtualProject::NonProject(workspace) => {
|
||||
Self::NonProject(workspace) => {
|
||||
// For non-projects, we might have dev dependencies that are attached to the
|
||||
// workspace root (which isn't a member).
|
||||
if name == &*DEV_DEPENDENCIES {
|
||||
|
@ -1395,17 +1434,22 @@ impl VirtualProject {
|
|||
}
|
||||
}
|
||||
|
||||
/// Return the [`PackageName`] of the project, if available.
|
||||
/// Return the [`PackageName`] of the target, if available.
|
||||
pub fn project_name(&self) -> Option<&PackageName> {
|
||||
match self {
|
||||
VirtualProject::Project(project) => Some(project.project_name()),
|
||||
VirtualProject::NonProject(_) => None,
|
||||
Self::Project(project) => Some(project.project_name()),
|
||||
Self::NonProject(_) => None,
|
||||
Self::FrozenMember(_, package_name) => Some(package_name),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the project is a virtual workspace root.
|
||||
pub fn is_non_project(&self) -> bool {
|
||||
matches!(self, VirtualProject::NonProject(_))
|
||||
impl<'env> From<&'env VirtualProject> for InstallTarget<'env> {
|
||||
fn from(project: &'env VirtualProject) -> Self {
|
||||
match project {
|
||||
VirtualProject::Project(project) => Self::Project(project),
|
||||
VirtualProject::NonProject(workspace) => Self::NonProject(workspace),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -30,7 +30,7 @@ use uv_types::{BuildIsolation, HashStrategy};
|
|||
use uv_warnings::warn_user_once;
|
||||
use uv_workspace::pyproject::{DependencyType, Source, SourceError};
|
||||
use uv_workspace::pyproject_mut::{ArrayEdit, DependencyTarget, PyProjectTomlMut};
|
||||
use uv_workspace::{DiscoveryOptions, VirtualProject, Workspace};
|
||||
use uv_workspace::{DiscoveryOptions, InstallTarget, VirtualProject, Workspace};
|
||||
|
||||
use crate::commands::pip::loggers::{
|
||||
DefaultInstallLogger, DefaultResolveLogger, SummaryResolveLogger,
|
||||
|
@ -668,7 +668,7 @@ pub(crate) async fn add(
|
|||
let install_options = InstallOptions::default();
|
||||
|
||||
if let Err(err) = project::sync::do_sync(
|
||||
&project,
|
||||
InstallTarget::from(&project),
|
||||
&venv,
|
||||
&lock,
|
||||
&extras,
|
||||
|
|
|
@ -13,7 +13,7 @@ use uv_scripts::Pep723Script;
|
|||
use uv_warnings::{warn_user, warn_user_once};
|
||||
use uv_workspace::pyproject::DependencyType;
|
||||
use uv_workspace::pyproject_mut::{DependencyTarget, PyProjectTomlMut};
|
||||
use uv_workspace::{DiscoveryOptions, VirtualProject, Workspace};
|
||||
use uv_workspace::{DiscoveryOptions, InstallTarget, VirtualProject, Workspace};
|
||||
|
||||
use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger};
|
||||
use crate::commands::pip::operations::Modifications;
|
||||
|
@ -196,7 +196,7 @@ pub(crate) async fn remove(
|
|||
let state = SharedState::default();
|
||||
|
||||
project::sync::do_sync(
|
||||
&project,
|
||||
InstallTarget::from(&project),
|
||||
&venv,
|
||||
&lock,
|
||||
&extras,
|
||||
|
|
|
@ -26,7 +26,7 @@ use uv_python::{
|
|||
use uv_requirements::{RequirementsSource, RequirementsSpecification};
|
||||
use uv_scripts::Pep723Script;
|
||||
use uv_warnings::warn_user;
|
||||
use uv_workspace::{DiscoveryOptions, VirtualProject, Workspace, WorkspaceError};
|
||||
use uv_workspace::{DiscoveryOptions, InstallTarget, VirtualProject, Workspace, WorkspaceError};
|
||||
|
||||
use crate::commands::pip::loggers::{
|
||||
DefaultInstallLogger, DefaultResolveLogger, SummaryInstallLogger, SummaryResolveLogger,
|
||||
|
@ -432,7 +432,7 @@ pub(crate) async fn run(
|
|||
uv_resolver::ResolveError::NoSolution(err),
|
||||
))) => {
|
||||
let report = miette::Report::msg(format!("{err}")).context(err.header());
|
||||
anstream::eprint!("{report:?}");
|
||||
eprint!("{report:?}");
|
||||
return Ok(ExitStatus::Failure);
|
||||
}
|
||||
Err(err) => return Err(err.into()),
|
||||
|
@ -441,7 +441,7 @@ pub(crate) async fn run(
|
|||
let install_options = InstallOptions::default();
|
||||
|
||||
project::sync::do_sync(
|
||||
&project,
|
||||
InstallTarget::from(&project),
|
||||
&venv,
|
||||
result.lock(),
|
||||
&extras,
|
||||
|
|
|
@ -14,7 +14,7 @@ use uv_normalize::{PackageName, DEV_DEPENDENCIES};
|
|||
use uv_python::{PythonDownloads, PythonEnvironment, PythonPreference, PythonRequest};
|
||||
use uv_resolver::{FlatIndex, Lock};
|
||||
use uv_types::{BuildIsolation, HashStrategy};
|
||||
use uv_workspace::{DiscoveryOptions, MemberDiscovery, VirtualProject, Workspace};
|
||||
use uv_workspace::{DiscoveryOptions, InstallTarget, MemberDiscovery, VirtualProject, Workspace};
|
||||
|
||||
use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger, InstallLogger};
|
||||
use crate::commands::pip::operations::Modifications;
|
||||
|
@ -45,14 +45,7 @@ pub(crate) async fn sync(
|
|||
printer: Printer,
|
||||
) -> Result<ExitStatus> {
|
||||
// Identify the project.
|
||||
let project = if let Some(package) = package {
|
||||
VirtualProject::Project(
|
||||
Workspace::discover(&CWD, &DiscoveryOptions::default())
|
||||
.await?
|
||||
.with_current_project(package.clone())
|
||||
.with_context(|| format!("Package `{package}` not found in workspace"))?,
|
||||
)
|
||||
} else if frozen {
|
||||
let project = if frozen {
|
||||
VirtualProject::discover(
|
||||
&CWD,
|
||||
&DiscoveryOptions {
|
||||
|
@ -61,13 +54,27 @@ pub(crate) async fn sync(
|
|||
},
|
||||
)
|
||||
.await?
|
||||
} else if let Some(package) = package.as_ref() {
|
||||
VirtualProject::Project(
|
||||
Workspace::discover(&CWD, &DiscoveryOptions::default())
|
||||
.await?
|
||||
.with_current_project(package.clone())
|
||||
.with_context(|| format!("Package `{package}` not found in workspace"))?,
|
||||
)
|
||||
} else {
|
||||
VirtualProject::discover(&CWD, &DiscoveryOptions::default()).await?
|
||||
};
|
||||
|
||||
// Identify the target.
|
||||
let target = if let Some(package) = package.as_ref().filter(|_| frozen) {
|
||||
InstallTarget::frozen_member(&project, package)
|
||||
} else {
|
||||
InstallTarget::from(&project)
|
||||
};
|
||||
|
||||
// Discover or create the virtual environment.
|
||||
let venv = project::get_or_init_environment(
|
||||
project.workspace(),
|
||||
target.workspace(),
|
||||
python.as_deref().map(PythonRequest::parse),
|
||||
python_preference,
|
||||
python_downloads,
|
||||
|
@ -81,7 +88,7 @@ pub(crate) async fn sync(
|
|||
let lock = match do_safe_lock(
|
||||
locked,
|
||||
frozen,
|
||||
project.workspace(),
|
||||
target.workspace(),
|
||||
venv.interpreter(),
|
||||
settings.as_ref().into(),
|
||||
Box::new(DefaultResolveLogger),
|
||||
|
@ -109,7 +116,7 @@ pub(crate) async fn sync(
|
|||
|
||||
// Perform the sync operation.
|
||||
do_sync(
|
||||
&project,
|
||||
target,
|
||||
&venv,
|
||||
&lock,
|
||||
&extras,
|
||||
|
@ -133,7 +140,7 @@ pub(crate) async fn sync(
|
|||
/// Sync a lockfile with an environment.
|
||||
#[allow(clippy::fn_params_excessive_bools)]
|
||||
pub(super) async fn do_sync(
|
||||
project: &VirtualProject,
|
||||
target: InstallTarget<'_>,
|
||||
venv: &PythonEnvironment,
|
||||
lock: &Lock,
|
||||
extras: &ExtrasSpecification,
|
||||
|
@ -205,14 +212,14 @@ pub(super) async fn do_sync(
|
|||
let tags = venv.interpreter().tags()?;
|
||||
|
||||
// Read the lockfile.
|
||||
let resolution = lock.to_resolution(project, &markers, tags, extras, &dev)?;
|
||||
let resolution = lock.to_resolution(target, &markers, tags, extras, &dev)?;
|
||||
|
||||
// Always skip virtual projects, which shouldn't be built or installed.
|
||||
let resolution = apply_no_virtual_project(resolution);
|
||||
|
||||
// Filter resolution based on install-specific options.
|
||||
let resolution =
|
||||
install_options.filter_resolution(resolution, project.project_name(), lock.members());
|
||||
install_options.filter_resolution(resolution, target.project_name(), lock.members());
|
||||
|
||||
// Add all authenticated sources to the cache.
|
||||
for url in index_locations.urls() {
|
||||
|
|
|
@ -1125,14 +1125,27 @@ fn no_install_workspace() -> Result<()> {
|
|||
+ sniffio==1.3.1
|
||||
"###);
|
||||
|
||||
// Unless `--package` is used.
|
||||
// Even if `--package` is used.
|
||||
uv_snapshot!(context.filters(), context.sync().arg("--package").arg("child").arg("--no-install-workspace").arg("--frozen"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Uninstalled 3 packages in [TIME]
|
||||
- anyio==3.7.0
|
||||
- idna==3.6
|
||||
- sniffio==1.3.1
|
||||
"###);
|
||||
|
||||
// Unless the package doesn't exist.
|
||||
uv_snapshot!(context.filters(), context.sync().arg("--package").arg("fake").arg("--no-install-workspace").arg("--frozen"), @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: Workspace member `[TEMP_DIR]/child` is missing a `pyproject.toml` (matches: `child`)
|
||||
error: could not find root package `fake`
|
||||
"###);
|
||||
|
||||
// But we do require the root `pyproject.toml`.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue