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:
chisato 2025-09-17 19:35:32 +08:00 committed by GitHub
parent eb5ec95396
commit accfb48876
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 271 additions and 17 deletions

View file

@ -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,

View file

@ -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;

View file

@ -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,

View file

@ -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;
} }

View file

@ -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 {

View file

@ -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 {

View file

@ -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),

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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(())
}

View file

@ -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(())
}