mirror of
https://github.com/astral-sh/uv.git
synced 2025-11-03 13:14:41 +00:00
Fix uv sync --no-sources not switching from editable to registry installations (#15234)
## Summary Fixes issue #15190 where `uv sync --no-sources` fails to switch from editable to registry package installations. The problem occurred because the installer's satisfaction check didn't consider the `--no-sources` flag when determining if an existing editable installation was compatible with a registry requirement. ## Solution Modified `RequirementSatisfaction::check()` to reject non-registry installations when `SourceStrategy::Disabled` and the requirement is from registry. Added `SourceStrategy` parameter threading through the entire call chain from commands to the satisfaction check to ensure consistent behavior between `uv sync --no-sources` and `uv pip install --no-sources`. --------- Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
This commit is contained in:
parent
eb5ec95396
commit
accfb48876
17 changed files with 271 additions and 17 deletions
|
|
@ -28,7 +28,7 @@ use uv_distribution_types::{
|
||||||
PackageConfigSettings, Requirement, Resolution, SourceDist, VersionOrUrlRef,
|
PackageConfigSettings, Requirement, Resolution, SourceDist, VersionOrUrlRef,
|
||||||
};
|
};
|
||||||
use uv_git::GitResolver;
|
use uv_git::GitResolver;
|
||||||
use uv_installer::{Installer, Plan, Planner, Preparer, SitePackages};
|
use uv_installer::{InstallationStrategy, Installer, Plan, Planner, Preparer, SitePackages};
|
||||||
use uv_preview::Preview;
|
use uv_preview::Preview;
|
||||||
use uv_pypi_types::Conflicts;
|
use uv_pypi_types::Conflicts;
|
||||||
use uv_python::{Interpreter, PythonEnvironment};
|
use uv_python::{Interpreter, PythonEnvironment};
|
||||||
|
|
@ -316,6 +316,7 @@ impl BuildContext for BuildDispatch<'_> {
|
||||||
extraneous: _,
|
extraneous: _,
|
||||||
} = Planner::new(resolution).build(
|
} = Planner::new(resolution).build(
|
||||||
site_packages,
|
site_packages,
|
||||||
|
InstallationStrategy::Permissive,
|
||||||
&Reinstall::default(),
|
&Reinstall::default(),
|
||||||
self.build_options,
|
self.build_options,
|
||||||
self.hasher,
|
self.hasher,
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,9 @@ pub use compile::{CompileError, compile_tree};
|
||||||
pub use installer::{Installer, Reporter as InstallReporter};
|
pub use installer::{Installer, Reporter as InstallReporter};
|
||||||
pub use plan::{Plan, Planner};
|
pub use plan::{Plan, Planner};
|
||||||
pub use preparer::{Error as PrepareError, Preparer, Reporter as PrepareReporter};
|
pub use preparer::{Error as PrepareError, Preparer, Reporter as PrepareReporter};
|
||||||
pub use site_packages::{SatisfiesResult, SitePackages, SitePackagesDiagnostic};
|
pub use site_packages::{
|
||||||
|
InstallationStrategy, SatisfiesResult, SitePackages, SitePackagesDiagnostic,
|
||||||
|
};
|
||||||
pub use uninstall::{UninstallError, uninstall};
|
pub use uninstall::{UninstallError, uninstall};
|
||||||
|
|
||||||
mod compile;
|
mod compile;
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,8 @@ use uv_pypi_types::VerbatimParsedUrl;
|
||||||
use uv_python::PythonEnvironment;
|
use uv_python::PythonEnvironment;
|
||||||
use uv_types::HashStrategy;
|
use uv_types::HashStrategy;
|
||||||
|
|
||||||
use crate::SitePackages;
|
|
||||||
use crate::satisfies::RequirementSatisfaction;
|
use crate::satisfies::RequirementSatisfaction;
|
||||||
|
use crate::{InstallationStrategy, SitePackages};
|
||||||
|
|
||||||
/// A planner to generate an [`Plan`] based on a set of requirements.
|
/// A planner to generate an [`Plan`] based on a set of requirements.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
|
@ -52,6 +52,7 @@ impl<'a> Planner<'a> {
|
||||||
pub fn build(
|
pub fn build(
|
||||||
self,
|
self,
|
||||||
mut site_packages: SitePackages,
|
mut site_packages: SitePackages,
|
||||||
|
installation: InstallationStrategy,
|
||||||
reinstall: &Reinstall,
|
reinstall: &Reinstall,
|
||||||
build_options: &BuildOptions,
|
build_options: &BuildOptions,
|
||||||
hasher: &HashStrategy,
|
hasher: &HashStrategy,
|
||||||
|
|
@ -125,6 +126,7 @@ impl<'a> Planner<'a> {
|
||||||
dist.name(),
|
dist.name(),
|
||||||
installed,
|
installed,
|
||||||
&source,
|
&source,
|
||||||
|
installation,
|
||||||
tags,
|
tags,
|
||||||
config_settings,
|
config_settings,
|
||||||
config_settings_package,
|
config_settings_package,
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,8 @@ use uv_normalize::PackageName;
|
||||||
use uv_platform_tags::{IncompatibleTag, TagCompatibility, Tags};
|
use uv_platform_tags::{IncompatibleTag, TagCompatibility, Tags};
|
||||||
use uv_pypi_types::{DirInfo, DirectUrl, VcsInfo, VcsKind};
|
use uv_pypi_types::{DirInfo, DirectUrl, VcsInfo, VcsKind};
|
||||||
|
|
||||||
|
use crate::InstallationStrategy;
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone)]
|
#[derive(Debug, Copy, Clone)]
|
||||||
pub(crate) enum RequirementSatisfaction {
|
pub(crate) enum RequirementSatisfaction {
|
||||||
Mismatch,
|
Mismatch,
|
||||||
|
|
@ -35,6 +37,7 @@ impl RequirementSatisfaction {
|
||||||
name: &PackageName,
|
name: &PackageName,
|
||||||
distribution: &InstalledDist,
|
distribution: &InstalledDist,
|
||||||
source: &RequirementSource,
|
source: &RequirementSource,
|
||||||
|
installation: InstallationStrategy,
|
||||||
tags: &Tags,
|
tags: &Tags,
|
||||||
config_settings: &ConfigSettings,
|
config_settings: &ConfigSettings,
|
||||||
config_settings_package: &PackageConfigSettings,
|
config_settings_package: &PackageConfigSettings,
|
||||||
|
|
@ -67,6 +70,26 @@ impl RequirementSatisfaction {
|
||||||
match source {
|
match source {
|
||||||
// If the requirement comes from a registry, check by name.
|
// If the requirement comes from a registry, check by name.
|
||||||
RequirementSource::Registry { specifier, .. } => {
|
RequirementSource::Registry { specifier, .. } => {
|
||||||
|
// If the installed distribution is _not_ from a registry, reject it if and only if
|
||||||
|
// we're in a stateless install.
|
||||||
|
//
|
||||||
|
// For example: the `uv pip` CLI is stateful, in that it "respects"
|
||||||
|
// already-installed packages in the virtual environment. So if you run `uv pip
|
||||||
|
// install ./path/to/idna`, and then `uv pip install anyio` (which depends on
|
||||||
|
// `idna`), we'll "accept" the already-installed `idna` even though it is implicitly
|
||||||
|
// being "required" as a registry package.
|
||||||
|
//
|
||||||
|
// The `uv sync` CLI is stateless, in that all requirements must be defined
|
||||||
|
// declaratively ahead-of-time. So if you `uv sync` to install `./path/to/idna` and
|
||||||
|
// later `uv sync` to install `anyio`, we'll know (during that second sync) if the
|
||||||
|
// already-installed `idna` should come from the registry or not.
|
||||||
|
if installation == InstallationStrategy::Strict {
|
||||||
|
if !matches!(distribution.kind, InstalledDistKind::Registry { .. }) {
|
||||||
|
debug!("Distribution type mismatch for {name}: {distribution:?}");
|
||||||
|
return Self::Mismatch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if !specifier.contains(distribution.version()) {
|
if !specifier.contains(distribution.version()) {
|
||||||
return Self::Mismatch;
|
return Self::Mismatch;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -314,6 +314,7 @@ impl SitePackages {
|
||||||
requirements: &[UnresolvedRequirementSpecification],
|
requirements: &[UnresolvedRequirementSpecification],
|
||||||
constraints: &[NameRequirementSpecification],
|
constraints: &[NameRequirementSpecification],
|
||||||
overrides: &[UnresolvedRequirementSpecification],
|
overrides: &[UnresolvedRequirementSpecification],
|
||||||
|
installation: InstallationStrategy,
|
||||||
markers: &ResolverMarkerEnvironment,
|
markers: &ResolverMarkerEnvironment,
|
||||||
tags: &Tags,
|
tags: &Tags,
|
||||||
config_settings: &ConfigSettings,
|
config_settings: &ConfigSettings,
|
||||||
|
|
@ -404,6 +405,7 @@ impl SitePackages {
|
||||||
requirements.iter().map(Cow::as_ref),
|
requirements.iter().map(Cow::as_ref),
|
||||||
constraints.iter().map(|constraint| &constraint.requirement),
|
constraints.iter().map(|constraint| &constraint.requirement),
|
||||||
overrides.iter().map(Cow::as_ref),
|
overrides.iter().map(Cow::as_ref),
|
||||||
|
installation,
|
||||||
markers,
|
markers,
|
||||||
tags,
|
tags,
|
||||||
config_settings,
|
config_settings,
|
||||||
|
|
@ -419,6 +421,7 @@ impl SitePackages {
|
||||||
requirements: impl ExactSizeIterator<Item = &'a Requirement>,
|
requirements: impl ExactSizeIterator<Item = &'a Requirement>,
|
||||||
constraints: impl Iterator<Item = &'a Requirement>,
|
constraints: impl Iterator<Item = &'a Requirement>,
|
||||||
overrides: impl Iterator<Item = &'a Requirement>,
|
overrides: impl Iterator<Item = &'a Requirement>,
|
||||||
|
installation: InstallationStrategy,
|
||||||
markers: &ResolverMarkerEnvironment,
|
markers: &ResolverMarkerEnvironment,
|
||||||
tags: &Tags,
|
tags: &Tags,
|
||||||
config_settings: &ConfigSettings,
|
config_settings: &ConfigSettings,
|
||||||
|
|
@ -482,6 +485,7 @@ impl SitePackages {
|
||||||
name,
|
name,
|
||||||
distribution,
|
distribution,
|
||||||
&requirement.source,
|
&requirement.source,
|
||||||
|
installation,
|
||||||
tags,
|
tags,
|
||||||
config_settings,
|
config_settings,
|
||||||
config_settings_package,
|
config_settings_package,
|
||||||
|
|
@ -504,6 +508,7 @@ impl SitePackages {
|
||||||
name,
|
name,
|
||||||
distribution,
|
distribution,
|
||||||
&constraint.source,
|
&constraint.source,
|
||||||
|
installation,
|
||||||
tags,
|
tags,
|
||||||
config_settings,
|
config_settings,
|
||||||
config_settings_package,
|
config_settings_package,
|
||||||
|
|
@ -560,6 +565,27 @@ impl SitePackages {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum InstallationStrategy {
|
||||||
|
/// A permissive installation strategy, which accepts existing installations even if the source
|
||||||
|
/// type differs, as in the `pip` and `uv pip` CLIs.
|
||||||
|
///
|
||||||
|
/// In this strategy, packages that are already installed in the environment may be reused if
|
||||||
|
/// they implicitly match the requirements. For example, if the user installs `./path/to/idna`,
|
||||||
|
/// then runs `uv pip install anyio` (which depends on `idna`), the existing `idna` installation
|
||||||
|
/// will be reused if its version matches the requirement, even though it was installed from a
|
||||||
|
/// path and is being implicitly requested from a registry.
|
||||||
|
Permissive,
|
||||||
|
|
||||||
|
/// A strict installation strategy, which requires that existing installations match the source
|
||||||
|
/// type, as in the `uv sync` CLI.
|
||||||
|
///
|
||||||
|
/// This strategy enforces that the installation source must match the requirement source.
|
||||||
|
/// It prevents reusing packages that were installed from different sources, ensuring
|
||||||
|
/// declarative and reproducible environments.
|
||||||
|
Strict,
|
||||||
|
}
|
||||||
|
|
||||||
/// We check if all requirements are already satisfied, recursing through the requirements tree.
|
/// We check if all requirements are already satisfied, recursing through the requirements tree.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum SatisfiesResult {
|
pub enum SatisfiesResult {
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,10 @@ use pubgrub::Range;
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use tracing::{debug, trace};
|
use tracing::{debug, trace};
|
||||||
|
|
||||||
use uv_configuration::IndexStrategy;
|
use uv_configuration::{IndexStrategy, SourceStrategy};
|
||||||
use uv_distribution_types::{CompatibleDist, IncompatibleDist, IncompatibleSource, IndexUrl};
|
use uv_distribution_types::{
|
||||||
|
CompatibleDist, IncompatibleDist, IncompatibleSource, IndexUrl, InstalledDistKind,
|
||||||
|
};
|
||||||
use uv_distribution_types::{DistributionMetadata, IncompatibleWheel, Name, PrioritizedDist};
|
use uv_distribution_types::{DistributionMetadata, IncompatibleWheel, Name, PrioritizedDist};
|
||||||
use uv_normalize::PackageName;
|
use uv_normalize::PackageName;
|
||||||
use uv_pep440::Version;
|
use uv_pep440::Version;
|
||||||
|
|
@ -26,6 +28,7 @@ pub(crate) struct CandidateSelector {
|
||||||
resolution_strategy: ResolutionStrategy,
|
resolution_strategy: ResolutionStrategy,
|
||||||
prerelease_strategy: PrereleaseStrategy,
|
prerelease_strategy: PrereleaseStrategy,
|
||||||
index_strategy: IndexStrategy,
|
index_strategy: IndexStrategy,
|
||||||
|
source_strategy: SourceStrategy,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CandidateSelector {
|
impl CandidateSelector {
|
||||||
|
|
@ -34,6 +37,7 @@ impl CandidateSelector {
|
||||||
options: &Options,
|
options: &Options,
|
||||||
manifest: &Manifest,
|
manifest: &Manifest,
|
||||||
env: &ResolverEnvironment,
|
env: &ResolverEnvironment,
|
||||||
|
source_strategy: SourceStrategy,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
resolution_strategy: ResolutionStrategy::from_mode(
|
resolution_strategy: ResolutionStrategy::from_mode(
|
||||||
|
|
@ -49,6 +53,7 @@ impl CandidateSelector {
|
||||||
options.dependency_mode,
|
options.dependency_mode,
|
||||||
),
|
),
|
||||||
index_strategy: options.index_strategy,
|
index_strategy: options.index_strategy,
|
||||||
|
source_strategy,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -119,7 +124,13 @@ impl CandidateSelector {
|
||||||
let installed = if reinstall {
|
let installed = if reinstall {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
Self::get_installed(package_name, range, installed_packages, tags)
|
Self::get_installed(
|
||||||
|
package_name,
|
||||||
|
range,
|
||||||
|
installed_packages,
|
||||||
|
tags,
|
||||||
|
self.source_strategy,
|
||||||
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
// If we're not upgrading, we should prefer the already-installed distribution.
|
// If we're not upgrading, we should prefer the already-installed distribution.
|
||||||
|
|
@ -369,6 +380,7 @@ impl CandidateSelector {
|
||||||
range: &Range<Version>,
|
range: &Range<Version>,
|
||||||
installed_packages: &'a InstalledPackages,
|
installed_packages: &'a InstalledPackages,
|
||||||
tags: Option<&'a Tags>,
|
tags: Option<&'a Tags>,
|
||||||
|
source_strategy: SourceStrategy,
|
||||||
) -> Option<Candidate<'a>> {
|
) -> Option<Candidate<'a>> {
|
||||||
let installed_dists = installed_packages.get_packages(package_name);
|
let installed_dists = installed_packages.get_packages(package_name);
|
||||||
match installed_dists.as_slice() {
|
match installed_dists.as_slice() {
|
||||||
|
|
@ -381,6 +393,16 @@ impl CandidateSelector {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// When sources are disabled, only allow registry installations to be reused
|
||||||
|
if matches!(source_strategy, SourceStrategy::Disabled) {
|
||||||
|
if !matches!(dist.kind, InstalledDistKind::Registry(_)) {
|
||||||
|
debug!(
|
||||||
|
"Source strategy is disabled, rejecting non-registry installed distribution: {dist}"
|
||||||
|
);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Verify that the installed distribution is compatible with the environment.
|
// Verify that the installed distribution is compatible with the environment.
|
||||||
if tags.is_some_and(|tags| {
|
if tags.is_some_and(|tags| {
|
||||||
let Ok(Some(wheel_tags)) = dist.read_tags() else {
|
let Ok(Some(wheel_tags)) = dist.read_tags() else {
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ use tokio::sync::oneshot;
|
||||||
use tokio_stream::wrappers::ReceiverStream;
|
use tokio_stream::wrappers::ReceiverStream;
|
||||||
use tracing::{Level, debug, info, instrument, trace, warn};
|
use tracing::{Level, debug, info, instrument, trace, warn};
|
||||||
|
|
||||||
use uv_configuration::{Constraints, Overrides};
|
use uv_configuration::{Constraints, Overrides, SourceStrategy};
|
||||||
use uv_distribution::{ArchiveMetadata, DistributionDatabase};
|
use uv_distribution::{ArchiveMetadata, DistributionDatabase};
|
||||||
use uv_distribution_types::{
|
use uv_distribution_types::{
|
||||||
BuiltDist, CompatibleDist, DerivationChain, Dist, DistErrorKind, DistributionMetadata,
|
BuiltDist, CompatibleDist, DerivationChain, Dist, DistErrorKind, DistributionMetadata,
|
||||||
|
|
@ -36,6 +36,7 @@ use uv_pep508::{
|
||||||
};
|
};
|
||||||
use uv_platform_tags::Tags;
|
use uv_platform_tags::Tags;
|
||||||
use uv_pypi_types::{ConflictItem, ConflictItemRef, ConflictKindRef, Conflicts, VerbatimParsedUrl};
|
use uv_pypi_types::{ConflictItem, ConflictItemRef, ConflictKindRef, Conflicts, VerbatimParsedUrl};
|
||||||
|
use uv_torch::TorchStrategy;
|
||||||
use uv_types::{BuildContext, HashStrategy, InstalledPackagesProvider};
|
use uv_types::{BuildContext, HashStrategy, InstalledPackagesProvider};
|
||||||
use uv_warnings::warn_user_once;
|
use uv_warnings::warn_user_once;
|
||||||
|
|
||||||
|
|
@ -82,7 +83,6 @@ use crate::{
|
||||||
marker,
|
marker,
|
||||||
};
|
};
|
||||||
pub(crate) use provider::MetadataUnavailable;
|
pub(crate) use provider::MetadataUnavailable;
|
||||||
use uv_torch::TorchStrategy;
|
|
||||||
|
|
||||||
mod availability;
|
mod availability;
|
||||||
mod batch_prefetch;
|
mod batch_prefetch;
|
||||||
|
|
@ -201,6 +201,7 @@ impl<'a, Context: BuildContext, InstalledPackages: InstalledPackagesProvider>
|
||||||
build_context.git(),
|
build_context.git(),
|
||||||
build_context.capabilities(),
|
build_context.capabilities(),
|
||||||
build_context.locations(),
|
build_context.locations(),
|
||||||
|
build_context.sources(),
|
||||||
provider,
|
provider,
|
||||||
installed_packages,
|
installed_packages,
|
||||||
)
|
)
|
||||||
|
|
@ -224,6 +225,7 @@ impl<Provider: ResolverProvider, InstalledPackages: InstalledPackagesProvider>
|
||||||
git: &GitResolver,
|
git: &GitResolver,
|
||||||
capabilities: &IndexCapabilities,
|
capabilities: &IndexCapabilities,
|
||||||
locations: &IndexLocations,
|
locations: &IndexLocations,
|
||||||
|
source_strategy: SourceStrategy,
|
||||||
provider: Provider,
|
provider: Provider,
|
||||||
installed_packages: InstalledPackages,
|
installed_packages: InstalledPackages,
|
||||||
) -> Result<Self, ResolveError> {
|
) -> Result<Self, ResolveError> {
|
||||||
|
|
@ -231,7 +233,7 @@ impl<Provider: ResolverProvider, InstalledPackages: InstalledPackagesProvider>
|
||||||
index: index.clone(),
|
index: index.clone(),
|
||||||
git: git.clone(),
|
git: git.clone(),
|
||||||
capabilities: capabilities.clone(),
|
capabilities: capabilities.clone(),
|
||||||
selector: CandidateSelector::for_resolution(&options, &manifest, &env),
|
selector: CandidateSelector::for_resolution(&options, &manifest, &env, source_strategy),
|
||||||
dependency_mode: options.dependency_mode,
|
dependency_mode: options.dependency_mode,
|
||||||
urls: Urls::from_manifest(&manifest, &env, git, options.dependency_mode),
|
urls: Urls::from_manifest(&manifest, &env, git, options.dependency_mode),
|
||||||
indexes: Indexes::from_manifest(&manifest, &env, options.dependency_mode),
|
indexes: Indexes::from_manifest(&manifest, &env, options.dependency_mode),
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ use uv_distribution_types::{
|
||||||
};
|
};
|
||||||
use uv_fs::Simplified;
|
use uv_fs::Simplified;
|
||||||
use uv_install_wheel::LinkMode;
|
use uv_install_wheel::LinkMode;
|
||||||
use uv_installer::{SatisfiesResult, SitePackages};
|
use uv_installer::{InstallationStrategy, SatisfiesResult, SitePackages};
|
||||||
use uv_normalize::{DefaultExtras, DefaultGroups};
|
use uv_normalize::{DefaultExtras, DefaultGroups};
|
||||||
use uv_preview::{Preview, PreviewFeatures};
|
use uv_preview::{Preview, PreviewFeatures};
|
||||||
use uv_pypi_types::Conflicts;
|
use uv_pypi_types::Conflicts;
|
||||||
|
|
@ -289,6 +289,7 @@ pub(crate) async fn pip_install(
|
||||||
&requirements,
|
&requirements,
|
||||||
&constraints,
|
&constraints,
|
||||||
&overrides,
|
&overrides,
|
||||||
|
InstallationStrategy::Permissive,
|
||||||
&marker_env,
|
&marker_env,
|
||||||
&tags,
|
&tags,
|
||||||
config_settings,
|
config_settings,
|
||||||
|
|
@ -602,6 +603,7 @@ pub(crate) async fn pip_install(
|
||||||
match operations::install(
|
match operations::install(
|
||||||
&resolution,
|
&resolution,
|
||||||
site_packages,
|
site_packages,
|
||||||
|
InstallationStrategy::Permissive,
|
||||||
modifications,
|
modifications,
|
||||||
&reinstall,
|
&reinstall,
|
||||||
&build_options,
|
&build_options,
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ use uv_distribution_types::{
|
||||||
use uv_distribution_types::{DistributionMetadata, InstalledMetadata, Name, Resolution};
|
use uv_distribution_types::{DistributionMetadata, InstalledMetadata, Name, Resolution};
|
||||||
use uv_fs::Simplified;
|
use uv_fs::Simplified;
|
||||||
use uv_install_wheel::LinkMode;
|
use uv_install_wheel::LinkMode;
|
||||||
use uv_installer::{Plan, Planner, Preparer, SitePackages};
|
use uv_installer::{InstallationStrategy, Plan, Planner, Preparer, SitePackages};
|
||||||
use uv_normalize::PackageName;
|
use uv_normalize::PackageName;
|
||||||
use uv_pep508::{MarkerEnvironment, RequirementOrigin};
|
use uv_pep508::{MarkerEnvironment, RequirementOrigin};
|
||||||
use uv_platform_tags::Tags;
|
use uv_platform_tags::Tags;
|
||||||
|
|
@ -436,6 +436,7 @@ impl Changelog {
|
||||||
pub(crate) async fn install(
|
pub(crate) async fn install(
|
||||||
resolution: &Resolution,
|
resolution: &Resolution,
|
||||||
site_packages: SitePackages,
|
site_packages: SitePackages,
|
||||||
|
installation: InstallationStrategy,
|
||||||
modifications: Modifications,
|
modifications: Modifications,
|
||||||
reinstall: &Reinstall,
|
reinstall: &Reinstall,
|
||||||
build_options: &BuildOptions,
|
build_options: &BuildOptions,
|
||||||
|
|
@ -462,6 +463,7 @@ pub(crate) async fn install(
|
||||||
let plan = Planner::new(resolution)
|
let plan = Planner::new(resolution)
|
||||||
.build(
|
.build(
|
||||||
site_packages,
|
site_packages,
|
||||||
|
installation,
|
||||||
reinstall,
|
reinstall,
|
||||||
build_options,
|
build_options,
|
||||||
hasher,
|
hasher,
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ use uv_distribution_types::{
|
||||||
};
|
};
|
||||||
use uv_fs::Simplified;
|
use uv_fs::Simplified;
|
||||||
use uv_install_wheel::LinkMode;
|
use uv_install_wheel::LinkMode;
|
||||||
use uv_installer::SitePackages;
|
use uv_installer::{InstallationStrategy, SitePackages};
|
||||||
use uv_normalize::{DefaultExtras, DefaultGroups};
|
use uv_normalize::{DefaultExtras, DefaultGroups};
|
||||||
use uv_preview::{Preview, PreviewFeatures};
|
use uv_preview::{Preview, PreviewFeatures};
|
||||||
use uv_pypi_types::Conflicts;
|
use uv_pypi_types::Conflicts;
|
||||||
|
|
@ -533,6 +533,7 @@ pub(crate) async fn pip_sync(
|
||||||
match operations::install(
|
match operations::install(
|
||||||
&resolution,
|
&resolution,
|
||||||
site_packages,
|
site_packages,
|
||||||
|
InstallationStrategy::Permissive,
|
||||||
Modifications::Exact,
|
Modifications::Exact,
|
||||||
&reinstall,
|
&reinstall,
|
||||||
&build_options,
|
&build_options,
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ use uv_distribution_types::{
|
||||||
};
|
};
|
||||||
use uv_fs::{CWD, LockedFile, Simplified};
|
use uv_fs::{CWD, LockedFile, Simplified};
|
||||||
use uv_git::ResolvedRepositoryReference;
|
use uv_git::ResolvedRepositoryReference;
|
||||||
use uv_installer::{SatisfiesResult, SitePackages};
|
use uv_installer::{InstallationStrategy, SatisfiesResult, SitePackages};
|
||||||
use uv_normalize::{DEV_DEPENDENCIES, DefaultGroups, ExtraName, GroupName, PackageName};
|
use uv_normalize::{DEV_DEPENDENCIES, DefaultGroups, ExtraName, GroupName, PackageName};
|
||||||
use uv_pep440::{TildeVersionSpecifier, Version, VersionSpecifiers};
|
use uv_pep440::{TildeVersionSpecifier, Version, VersionSpecifiers};
|
||||||
use uv_pep508::MarkerTreeContents;
|
use uv_pep508::MarkerTreeContents;
|
||||||
|
|
@ -2132,6 +2132,7 @@ pub(crate) async fn sync_environment(
|
||||||
pip::operations::install(
|
pip::operations::install(
|
||||||
resolution,
|
resolution,
|
||||||
site_packages,
|
site_packages,
|
||||||
|
InstallationStrategy::Permissive,
|
||||||
modifications,
|
modifications,
|
||||||
reinstall,
|
reinstall,
|
||||||
build_options,
|
build_options,
|
||||||
|
|
@ -2251,6 +2252,7 @@ pub(crate) async fn update_environment(
|
||||||
&requirements,
|
&requirements,
|
||||||
&constraints,
|
&constraints,
|
||||||
&overrides,
|
&overrides,
|
||||||
|
InstallationStrategy::Permissive,
|
||||||
&marker_env,
|
&marker_env,
|
||||||
&tags,
|
&tags,
|
||||||
config_setting,
|
config_setting,
|
||||||
|
|
@ -2396,6 +2398,7 @@ pub(crate) async fn update_environment(
|
||||||
let changelog = pip::operations::install(
|
let changelog = pip::operations::install(
|
||||||
&resolution,
|
&resolution,
|
||||||
site_packages,
|
site_packages,
|
||||||
|
InstallationStrategy::Permissive,
|
||||||
modifications,
|
modifications,
|
||||||
reinstall,
|
reinstall,
|
||||||
build_options,
|
build_options,
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ use uv_distribution::LoweredExtraBuildDependencies;
|
||||||
use uv_distribution_types::Requirement;
|
use uv_distribution_types::Requirement;
|
||||||
use uv_fs::which::is_executable;
|
use uv_fs::which::is_executable;
|
||||||
use uv_fs::{PythonExt, Simplified, create_symlink};
|
use uv_fs::{PythonExt, Simplified, create_symlink};
|
||||||
use uv_installer::{SatisfiesResult, SitePackages};
|
use uv_installer::{InstallationStrategy, SatisfiesResult, SitePackages};
|
||||||
use uv_normalize::{DefaultExtras, DefaultGroups, PackageName};
|
use uv_normalize::{DefaultExtras, DefaultGroups, PackageName};
|
||||||
use uv_preview::Preview;
|
use uv_preview::Preview;
|
||||||
use uv_python::{
|
use uv_python::{
|
||||||
|
|
@ -1360,6 +1360,7 @@ fn can_skip_ephemeral(
|
||||||
&spec.requirements,
|
&spec.requirements,
|
||||||
&spec.constraints,
|
&spec.constraints,
|
||||||
&spec.overrides,
|
&spec.overrides,
|
||||||
|
InstallationStrategy::Permissive,
|
||||||
&markers,
|
&markers,
|
||||||
tags,
|
tags,
|
||||||
config_setting,
|
config_setting,
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ use uv_distribution_types::{
|
||||||
DirectorySourceDist, Dist, Index, Requirement, Resolution, ResolvedDist, SourceDist,
|
DirectorySourceDist, Dist, Index, Requirement, Resolution, ResolvedDist, SourceDist,
|
||||||
};
|
};
|
||||||
use uv_fs::{PortablePathBuf, Simplified};
|
use uv_fs::{PortablePathBuf, Simplified};
|
||||||
use uv_installer::SitePackages;
|
use uv_installer::{InstallationStrategy, SitePackages};
|
||||||
use uv_normalize::{DefaultExtras, DefaultGroups, PackageName};
|
use uv_normalize::{DefaultExtras, DefaultGroups, PackageName};
|
||||||
use uv_pep508::{MarkerTree, VersionOrUrl};
|
use uv_pep508::{MarkerTree, VersionOrUrl};
|
||||||
use uv_preview::{Preview, PreviewFeatures};
|
use uv_preview::{Preview, PreviewFeatures};
|
||||||
|
|
@ -782,6 +782,7 @@ pub(super) async fn do_sync(
|
||||||
operations::install(
|
operations::install(
|
||||||
&resolution,
|
&resolution,
|
||||||
site_packages,
|
site_packages,
|
||||||
|
InstallationStrategy::Strict,
|
||||||
modifications,
|
modifications,
|
||||||
reinstall,
|
reinstall,
|
||||||
build_options,
|
build_options,
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ use uv_distribution_types::{
|
||||||
ExtraBuildRequires, NameRequirementSpecification, Requirement, RequirementSource,
|
ExtraBuildRequires, NameRequirementSpecification, Requirement, RequirementSource,
|
||||||
UnresolvedRequirementSpecification,
|
UnresolvedRequirementSpecification,
|
||||||
};
|
};
|
||||||
use uv_installer::{SatisfiesResult, SitePackages};
|
use uv_installer::{InstallationStrategy, SatisfiesResult, SitePackages};
|
||||||
use uv_normalize::PackageName;
|
use uv_normalize::PackageName;
|
||||||
use uv_pep440::{VersionSpecifier, VersionSpecifiers};
|
use uv_pep440::{VersionSpecifier, VersionSpecifiers};
|
||||||
use uv_pep508::MarkerTree;
|
use uv_pep508::MarkerTree;
|
||||||
|
|
@ -405,6 +405,7 @@ pub(crate) async fn install(
|
||||||
requirements.iter(),
|
requirements.iter(),
|
||||||
constraints.iter(),
|
constraints.iter(),
|
||||||
overrides.iter(),
|
overrides.iter(),
|
||||||
|
InstallationStrategy::Permissive,
|
||||||
&markers,
|
&markers,
|
||||||
&tags,
|
&tags,
|
||||||
config_setting,
|
config_setting,
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ use uv_distribution_types::{
|
||||||
UnresolvedRequirement, UnresolvedRequirementSpecification,
|
UnresolvedRequirement, UnresolvedRequirementSpecification,
|
||||||
};
|
};
|
||||||
use uv_fs::Simplified;
|
use uv_fs::Simplified;
|
||||||
use uv_installer::{SatisfiesResult, SitePackages};
|
use uv_installer::{InstallationStrategy, SatisfiesResult, SitePackages};
|
||||||
use uv_normalize::PackageName;
|
use uv_normalize::PackageName;
|
||||||
use uv_pep440::{VersionSpecifier, VersionSpecifiers};
|
use uv_pep440::{VersionSpecifier, VersionSpecifiers};
|
||||||
use uv_pep508::MarkerTree;
|
use uv_pep508::MarkerTree;
|
||||||
|
|
@ -973,6 +973,7 @@ async fn get_or_create_environment(
|
||||||
requirements.iter(),
|
requirements.iter(),
|
||||||
constraints.iter(),
|
constraints.iter(),
|
||||||
overrides.iter(),
|
overrides.iter(),
|
||||||
|
InstallationStrategy::Permissive,
|
||||||
&markers,
|
&markers,
|
||||||
&tags,
|
&tags,
|
||||||
config_setting,
|
config_setting,
|
||||||
|
|
|
||||||
|
|
@ -12845,3 +12845,67 @@ fn switch_platform() {
|
||||||
"
|
"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `uv pip install --no-sources` should allow non-registry installations, for compatibility with `pip install`.
|
||||||
|
///
|
||||||
|
/// See: <https://github.com/astral-sh/uv/issues/15190>
|
||||||
|
#[test]
|
||||||
|
fn pip_install_no_sources_editable_to_registry_switch() -> Result<()> {
|
||||||
|
let context = TestContext::new("3.12");
|
||||||
|
|
||||||
|
// Create a simple local package.
|
||||||
|
let local_pkg = context.temp_dir.child("local_pkg");
|
||||||
|
local_pkg.create_dir_all()?;
|
||||||
|
|
||||||
|
local_pkg.child("pyproject.toml").write_str(
|
||||||
|
r#"
|
||||||
|
[project]
|
||||||
|
name = "iniconfig"
|
||||||
|
version = "2.0.0"
|
||||||
|
description = "Local test package"
|
||||||
|
requires-python = ">=3.7"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
local_pkg.child("src").child("iniconfig").create_dir_all()?;
|
||||||
|
local_pkg
|
||||||
|
.child("src")
|
||||||
|
.child("iniconfig")
|
||||||
|
.child("__init__.py")
|
||||||
|
.write_str("__version__ = '2.0.0'")?;
|
||||||
|
|
||||||
|
// Step 1: Install as editable first.
|
||||||
|
uv_snapshot!(context.filters(), context.pip_install()
|
||||||
|
.arg("--editable")
|
||||||
|
.arg("./local_pkg"), @r"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved 1 package in [TIME]
|
||||||
|
Prepared 1 package in [TIME]
|
||||||
|
Installed 1 package in [TIME]
|
||||||
|
+ iniconfig==2.0.0 (from file://[TEMP_DIR]/local_pkg)
|
||||||
|
"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Step 2: Use `--no-sources`; we should retain the package.
|
||||||
|
uv_snapshot!(context.filters(), context.pip_install()
|
||||||
|
.arg("iniconfig")
|
||||||
|
.arg("--no-sources"), @r"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Audited 1 package in [TIME]
|
||||||
|
"
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -14151,3 +14151,103 @@ fn only_group_and_extra_conflict() -> Result<()> {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `uv sync --no-sources` should consistently switch from editable to package installation.
|
||||||
|
///
|
||||||
|
/// See: <https://github.com/astral-sh/uv/issues/15190>
|
||||||
|
#[test]
|
||||||
|
fn sync_no_sources_editable_to_package_switch() -> Result<()> {
|
||||||
|
let context = TestContext::new("3.12");
|
||||||
|
|
||||||
|
// Create a local package that will be used as editable dependency.
|
||||||
|
let local_dep = context.temp_dir.child("local_dep");
|
||||||
|
local_dep.create_dir_all()?;
|
||||||
|
|
||||||
|
let local_dep_pyproject = local_dep.child("pyproject.toml");
|
||||||
|
local_dep_pyproject.write_str(
|
||||||
|
r#"
|
||||||
|
[project]
|
||||||
|
name = "anyio"
|
||||||
|
version = "4.3.0"
|
||||||
|
description = "Local test package mimicking anyio"
|
||||||
|
requires-python = ">=3.8"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=61", "wheel"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Create main project with editable source for the local dependency.
|
||||||
|
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||||
|
pyproject_toml.write_str(
|
||||||
|
r#"
|
||||||
|
[project]
|
||||||
|
name = "test_no_sources"
|
||||||
|
version = "0.0.1"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
dependencies = ["anyio"]
|
||||||
|
|
||||||
|
[tool.uv.sources]
|
||||||
|
anyio = { path = "./local_dep", editable = true }
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=67"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[tool.setuptools.packages.find]
|
||||||
|
exclude = ["local_dep*"]
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Step 1: `uv sync --no-sources` should install `anyio` from PyPI.
|
||||||
|
uv_snapshot!(context.filters(), context.sync().arg("--no-sources"), @r"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved 4 packages in [TIME]
|
||||||
|
Prepared 4 packages in [TIME]
|
||||||
|
Installed 4 packages in [TIME]
|
||||||
|
+ anyio==4.3.0
|
||||||
|
+ idna==3.6
|
||||||
|
+ sniffio==1.3.1
|
||||||
|
+ test-no-sources==0.0.1 (from file://[TEMP_DIR]/)
|
||||||
|
");
|
||||||
|
|
||||||
|
// Step 2: `uv sync` should switch to an editable installation.
|
||||||
|
uv_snapshot!(context.filters(), context.sync(), @r"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved 2 packages in [TIME]
|
||||||
|
Prepared 1 package in [TIME]
|
||||||
|
Uninstalled 3 packages in [TIME]
|
||||||
|
Installed 1 package in [TIME]
|
||||||
|
- anyio==4.3.0
|
||||||
|
+ anyio==4.3.0 (from file://[TEMP_DIR]/local_dep)
|
||||||
|
- idna==3.6
|
||||||
|
- sniffio==1.3.1
|
||||||
|
");
|
||||||
|
|
||||||
|
// Step 3: `uv sync --no-sources` again should switch back to PyPI package.
|
||||||
|
uv_snapshot!(context.filters(), context.sync().arg("--no-sources"), @r"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved 4 packages in [TIME]
|
||||||
|
Uninstalled 1 package in [TIME]
|
||||||
|
Installed 3 packages in [TIME]
|
||||||
|
- anyio==4.3.0 (from file://[TEMP_DIR]/local_dep)
|
||||||
|
+ anyio==4.3.0
|
||||||
|
+ idna==3.6
|
||||||
|
+ sniffio==1.3.1
|
||||||
|
");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue