Remove special-casing for editable requirements (#3869)

## Summary

There are a few behavior changes in here:

- We now enforce `--require-hashes` for editables, like pip. So if you
use `--require-hashes` with an editable requirement, we'll reject it. I
could change this if it seems off.
- We now treat source tree requirements, editable or not (e.g., both `-e
./black` and `./black`) as if `--refresh` is always enabled. This
doesn't mean that we _always_ rebuild them; but if you pass
`--reinstall`, then yes, we always rebuild them. I think this is an
improvement and is close to how editables work today.

Closes #3844.

Closes #2695.
This commit is contained in:
Charlie Marsh 2024-05-28 11:49:34 -04:00 committed by GitHub
parent 063a0a4384
commit 1fc6a59707
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
64 changed files with 583 additions and 1813 deletions

3
Cargo.lock generated
View file

@ -4486,7 +4486,6 @@ dependencies = [
"filetime",
"flate2",
"fs-err",
"indexmap",
"indicatif",
"indoc",
"insta",
@ -4501,7 +4500,6 @@ dependencies = [
"pypi-types",
"rayon",
"regex",
"requirements-txt",
"reqwest",
"rustc-hash",
"serde",
@ -4868,7 +4866,6 @@ dependencies = [
"platform-tags",
"pypi-types",
"rayon",
"requirements-txt",
"rustc-hash",
"serde",
"tempfile",

View file

@ -1,8 +1,6 @@
use std::collections::{BTreeMap, BTreeSet};
use url::Url;
use pep508_rs::{RequirementOrigin, VerbatimUrl};
use pep508_rs::RequirementOrigin;
use uv_fs::Simplified;
use uv_normalize::PackageName;
@ -40,35 +38,19 @@ impl std::fmt::Display for SourceAnnotation {
/// A collection of source annotations.
#[derive(Default, Debug, Clone)]
pub struct SourceAnnotations {
packages: BTreeMap<PackageName, BTreeSet<SourceAnnotation>>,
editables: BTreeMap<Url, BTreeSet<SourceAnnotation>>,
}
pub struct SourceAnnotations(BTreeMap<PackageName, BTreeSet<SourceAnnotation>>);
impl SourceAnnotations {
/// Add a source annotation to the collection for the given package.
pub fn add(&mut self, package: &PackageName, annotation: SourceAnnotation) {
self.packages
self.0
.entry(package.clone())
.or_default()
.insert(annotation);
}
/// Add an source annotation to the collection for the given editable.
pub fn add_editable(&mut self, url: &VerbatimUrl, annotation: SourceAnnotation) {
self.editables
.entry(url.to_url())
.or_default()
.insert(annotation);
}
/// Return the source annotations for a given package.
pub fn get(&self, package: &PackageName) -> Option<&BTreeSet<SourceAnnotation>> {
self.packages.get(package)
}
/// Return the source annotations for a given editable.
pub fn get_editable(&self, url: &VerbatimUrl) -> Option<&BTreeSet<SourceAnnotation>> {
self.editables.get(url.raw())
self.0.get(package)
}
}

View file

@ -47,6 +47,14 @@ impl BuildableSource<'_> {
Self::Url(_) => None,
}
}
/// Returns `true` if the source is editable.
pub fn is_editable(&self) -> bool {
match self {
Self::Dist(dist) => dist.is_editable(),
Self::Url(url) => url.is_editable(),
}
}
}
impl std::fmt::Display for BuildableSource<'_> {
@ -77,6 +85,14 @@ impl<'a> SourceUrl<'a> {
Self::Directory(dist) => dist.url,
}
}
/// Returns `true` if the source is editable.
pub fn is_editable(&self) -> bool {
matches!(
self,
Self::Directory(DirectorySourceUrl { editable: true, .. })
)
}
}
impl std::fmt::Display for SourceUrl<'_> {
@ -151,6 +167,7 @@ impl<'a> From<&'a PathSourceDist> for PathSourceUrl<'a> {
pub struct DirectorySourceUrl<'a> {
pub url: &'a Url,
pub path: Cow<'a, Path>,
pub editable: bool,
}
impl std::fmt::Display for DirectorySourceUrl<'_> {
@ -164,6 +181,7 @@ impl<'a> From<&'a DirectorySourceDist> for DirectorySourceUrl<'a> {
Self {
url: &dist.url,
path: Cow::Borrowed(&dist.path),
editable: dist.editable,
}
}
}

View file

@ -131,14 +131,6 @@ impl CachedDist {
}
}
/// Returns `true` if the distribution is editable.
pub fn editable(&self) -> bool {
match self {
Self::Registry(_) => false,
Self::Url(dist) => dist.editable,
}
}
/// Returns the [`WheelFilename`] of the distribution.
pub fn filename(&self) -> &WheelFilename {
match self {

View file

@ -1,95 +0,0 @@
use std::borrow::Cow;
use std::collections::btree_map::Entry;
use std::collections::BTreeMap;
use std::path::PathBuf;
use url::Url;
use pep508_rs::VerbatimUrl;
use uv_normalize::ExtraName;
use crate::Verbatim;
#[derive(Debug, Clone)]
pub struct LocalEditable {
/// The underlying [`EditableRequirement`] from the `requirements.txt` file.
pub url: VerbatimUrl,
/// Either the path to the editable or its checkout.
pub path: PathBuf,
/// The extras that should be installed.
pub extras: Vec<ExtraName>,
}
impl LocalEditable {
/// Return the editable as a [`Url`].
pub fn url(&self) -> &VerbatimUrl {
&self.url
}
/// Return the resolved path to the editable.
pub fn raw(&self) -> &Url {
self.url.raw()
}
}
impl Verbatim for LocalEditable {
fn verbatim(&self) -> Cow<'_, str> {
self.url.verbatim()
}
}
impl std::fmt::Display for LocalEditable {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(&self.url, f)
}
}
/// A collection of [`LocalEditable`]s.
#[derive(Debug, Clone)]
pub struct LocalEditables(Vec<LocalEditable>);
impl LocalEditables {
/// Merge and dedupe a list of [`LocalEditable`]s.
///
/// This function will deduplicate any editables that point to identical paths, merging their
/// extras.
pub fn from_editables(editables: impl Iterator<Item = LocalEditable>) -> Self {
let mut map = BTreeMap::new();
for editable in editables {
match map.entry(editable.path.clone()) {
Entry::Vacant(entry) => {
entry.insert(editable);
}
Entry::Occupied(mut entry) => {
let existing = entry.get_mut();
existing.extras.extend(editable.extras);
}
}
}
Self(map.into_values().collect())
}
/// Return the number of editables.
pub fn len(&self) -> usize {
self.0.len()
}
/// Return whether the editables are empty.
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
/// Return the editables as a vector.
pub fn into_vec(self) -> Vec<LocalEditable> {
self.0
}
}
impl IntoIterator for LocalEditables {
type Item = LocalEditable;
type IntoIter = std::vec::IntoIter<LocalEditable>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}

View file

@ -267,12 +267,10 @@ impl InstalledDist {
/// Return true if the distribution is editable.
pub fn is_editable(&self) -> bool {
match self {
Self::Registry(_) => false,
Self::Url(dist) => dist.editable,
Self::EggInfo(_) => false,
Self::LegacyEditable(_) => true,
}
matches!(
self,
Self::LegacyEditable(_) | Self::Url(InstalledDirectUrlDist { editable: true, .. })
)
}
/// Return the [`Url`] of the distribution, if it is editable.

View file

@ -51,7 +51,6 @@ pub use crate::any::*;
pub use crate::buildable::*;
pub use crate::cached::*;
pub use crate::diagnostic::*;
pub use crate::editable::*;
pub use crate::error::*;
pub use crate::file::*;
pub use crate::hash::*;
@ -70,7 +69,6 @@ mod any;
mod buildable;
mod cached;
mod diagnostic;
mod editable;
mod error;
mod file;
mod hash;
@ -401,24 +399,15 @@ impl Dist {
ParsedUrl::Archive(archive) => {
Self::from_http_url(name, url.verbatim, archive.url, archive.subdirectory)
}
ParsedUrl::Path(file) => Self::from_file_url(name, url.verbatim, &file.path, false),
ParsedUrl::Path(file) => {
Self::from_file_url(name, url.verbatim, &file.path, file.editable)
}
ParsedUrl::Git(git) => {
Self::from_git_url(name, url.verbatim, git.url, git.subdirectory)
}
}
}
/// Create a [`Dist`] for a local editable distribution.
pub fn from_editable(name: PackageName, editable: LocalEditable) -> Result<Self, Error> {
let LocalEditable { url, path, .. } = editable;
Ok(Self::Source(SourceDist::Directory(DirectorySourceDist {
name,
url,
path,
editable: true,
})))
}
/// Return true if the distribution is editable.
pub fn is_editable(&self) -> bool {
match self {

View file

@ -44,6 +44,11 @@ impl Requirement {
true
}
}
/// Returns `true` if the requirement is editable.
pub fn is_editable(&self) -> bool {
self.source.is_editable()
}
}
impl From<pep508_rs::Requirement<VerbatimParsedUrl>> for Requirement {
@ -190,7 +195,7 @@ impl RequirementSource {
ParsedUrl::Path(local_file) => RequirementSource::Path {
path: local_file.path,
url,
editable: false,
editable: local_file.editable,
},
ParsedUrl::Git(git) => RequirementSource::Git {
url,

View file

@ -1,11 +1,9 @@
use std::collections::BTreeMap;
use pep508_rs::VerbatimUrl;
use uv_normalize::{ExtraName, PackageName};
use crate::{
BuiltDist, Diagnostic, DirectorySourceDist, Dist, InstalledDirectUrlDist, InstalledDist,
LocalEditable, Name, Requirement, RequirementSource, ResolvedDist, SourceDist,
BuiltDist, Diagnostic, Dist, Name, Requirement, RequirementSource, ResolvedDist, SourceDist,
};
/// A set of packages pinned at specific versions.
@ -67,35 +65,6 @@ impl Resolution {
pub fn diagnostics(&self) -> &[ResolutionDiagnostic] {
&self.diagnostics
}
/// Return an iterator over the [`LocalEditable`] entities in this resolution.
pub fn editables(&self) -> impl Iterator<Item = LocalEditable> + '_ {
self.packages.values().filter_map(|dist| match dist {
ResolvedDist::Installable(Dist::Source(SourceDist::Directory(
DirectorySourceDist {
path,
url,
editable: true,
..
},
))) => Some(LocalEditable {
url: url.clone(),
path: path.clone(),
extras: vec![],
}),
ResolvedDist::Installed(InstalledDist::Url(InstalledDirectUrlDist {
path,
url,
editable: true,
..
})) => Some(LocalEditable {
url: VerbatimUrl::from_url(url.clone()),
path: path.clone(),
extras: vec![],
}),
_ => None,
})
}
}
#[derive(Debug, Clone)]

View file

@ -73,4 +73,21 @@ impl UnresolvedRequirement {
)),
}
}
/// Returns `true` if the requirement is editable.
pub fn is_editable(&self) -> bool {
match self {
Self::Named(requirement) => requirement.is_editable(),
Self::Unnamed(requirement) => requirement.url.is_editable(),
}
}
}
impl From<Requirement> for UnresolvedRequirementSpecification {
fn from(requirement: Requirement) -> Self {
Self {
requirement: UnresolvedRequirement::Named(requirement),
hashes: Vec::new(),
}
}
}

View file

@ -33,6 +33,13 @@ pub struct VerbatimParsedUrl {
pub verbatim: VerbatimUrl,
}
impl VerbatimParsedUrl {
/// Returns `true` if the URL is editable.
pub fn is_editable(&self) -> bool {
self.parsed_url.is_editable()
}
}
impl Pep508Url for VerbatimParsedUrl {
type Err = ParsedUrlError;
@ -150,6 +157,13 @@ pub enum ParsedUrl {
Archive(ParsedArchiveUrl),
}
impl ParsedUrl {
/// Returns `true` if the URL is editable.
pub fn is_editable(&self) -> bool {
matches!(self, Self::Path(ParsedPathUrl { editable: true, .. }))
}
}
/// A local path url
///
/// Examples:

View file

@ -47,9 +47,9 @@ use url::Url;
use distribution_types::{Requirement, UnresolvedRequirement, UnresolvedRequirementSpecification};
use pep508_rs::{
expand_env_vars, split_scheme, strip_host, Extras, MarkerTree, Pep508Error, Pep508ErrorSource,
RequirementOrigin, Scheme, VerbatimUrl,
RequirementOrigin, Scheme, UnnamedRequirement, VerbatimUrl,
};
use pypi_types::VerbatimParsedUrl;
use pypi_types::{ParsedPathUrl, ParsedUrl, VerbatimParsedUrl};
#[cfg(feature = "http")]
use uv_client::BaseClient;
use uv_client::BaseClientBuilder;
@ -418,6 +418,27 @@ impl From<RequirementEntry> for UnresolvedRequirementSpecification {
}
}
impl From<EditableRequirement> for UnresolvedRequirementSpecification {
fn from(value: EditableRequirement) -> Self {
Self {
requirement: UnresolvedRequirement::Unnamed(UnnamedRequirement {
url: VerbatimParsedUrl {
parsed_url: ParsedUrl::Path(ParsedPathUrl {
url: value.url.to_url(),
path: value.path,
editable: true,
}),
verbatim: value.url,
},
extras: value.extras,
marker: value.marker,
origin: value.origin,
}),
hashes: vec![],
}
}
}
/// Parsed and flattened requirements.txt with requirements and constraints
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct RequirementsTxt {

View file

@ -129,7 +129,7 @@ impl Cache {
pub fn from_path(root: impl Into<PathBuf>) -> Self {
Self {
root: root.into(),
refresh: Refresh::None,
refresh: Refresh::None(Timestamp::now()),
_temp_dir_drop: None,
}
}
@ -139,7 +139,7 @@ impl Cache {
let temp_dir = tempdir()?;
Ok(Self {
root: temp_dir.path().to_path_buf(),
refresh: Refresh::None,
refresh: Refresh::None(Timestamp::now()),
_temp_dir_drop: Some(Arc::new(temp_dir)),
})
}
@ -183,13 +183,16 @@ impl Cache {
/// Returns `true` if a cache entry must be revalidated given the [`Refresh`] policy.
pub fn must_revalidate(&self, package: &PackageName) -> bool {
match &self.refresh {
Refresh::None => false,
Refresh::None(_) => false,
Refresh::All(_) => true,
Refresh::Packages(packages, _) => packages.contains(package),
}
}
/// Returns `true` if a cache entry is up-to-date given the [`Refresh`] policy.
/// Returns the [`Freshness`] for a cache entry, validating it against the [`Refresh`] policy.
///
/// A cache entry is considered fresh if it was created after the cache itself was
/// initialized, or if the [`Refresh`] policy does not require revalidation.
pub fn freshness(
&self,
entry: &CacheEntry,
@ -197,7 +200,7 @@ impl Cache {
) -> io::Result<Freshness> {
// Grab the cutoff timestamp, if it's relevant.
let timestamp = match &self.refresh {
Refresh::None => return Ok(Freshness::Fresh),
Refresh::None(_) => return Ok(Freshness::Fresh),
Refresh::All(timestamp) => timestamp,
Refresh::Packages(packages, timestamp) => {
if package.map_or(true, |package| packages.contains(package)) {
@ -221,6 +224,26 @@ impl Cache {
}
}
/// Returns `true` if a cache entry is up-to-date. Unlike [`Cache::freshness`], this method does
/// not take the [`Refresh`] policy into account.
///
/// A cache entry is considered up-to-date if it was created after the [`Cache`] instance itself
/// was initialized.
pub fn is_fresh(&self, entry: &CacheEntry) -> io::Result<bool> {
// Grab the cutoff timestamp.
let timestamp = match &self.refresh {
Refresh::None(timestamp) => timestamp,
Refresh::All(timestamp) => timestamp,
Refresh::Packages(_packages, timestamp) => timestamp,
};
match fs::metadata(entry.path()) {
Ok(metadata) => Ok(Timestamp::from_metadata(&metadata) >= *timestamp),
Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(false),
Err(err) => Err(err),
}
}
/// Persist a temporary directory to the artifact store, returning its unique ID.
pub async fn persist(
&self,
@ -897,10 +920,13 @@ impl Freshness {
}
/// A refresh policy for cache entries.
///
/// Each policy stores a timestamp, even if no entries are refreshed, to enable out-of-policy
/// freshness checks via [`Cache::is_fresh`].
#[derive(Debug, Clone)]
pub enum Refresh {
/// Don't refresh any entries.
None,
None(Timestamp),
/// Refresh entries linked to the given packages, if created before the given timestamp.
Packages(Vec<PackageName>, Timestamp),
/// Refresh all entries created before the given timestamp.
@ -910,14 +936,15 @@ pub enum Refresh {
impl Refresh {
/// Determine the refresh strategy to use based on the command-line arguments.
pub fn from_args(refresh: Option<bool>, refresh_package: Vec<PackageName>) -> Self {
let timestamp = Timestamp::now();
match refresh {
Some(true) => Self::All(Timestamp::now()),
Some(false) => Self::None,
Some(true) => Self::All(timestamp),
Some(false) => Self::None(timestamp),
None => {
if refresh_package.is_empty() {
Self::None
Self::None(timestamp)
} else {
Self::Packages(refresh_package, Timestamp::now())
Self::Packages(refresh_package, timestamp)
}
}
}
@ -925,6 +952,6 @@ impl Refresh {
/// Returns `true` if no packages should be reinstalled.
pub fn is_none(&self) -> bool {
matches!(self, Self::None)
matches!(self, Self::None(_))
}
}

View file

@ -14,6 +14,8 @@ pub enum WheelCache<'a> {
Url(&'a Url),
/// A path dependency, which we key by URL.
Path(&'a Url),
/// An editable dependency, which we key by URL.
Editable(&'a Url),
/// A Git dependency, which we key by URL and SHA.
///
/// Note that this variant only exists for source distributions; wheels can't be delivered
@ -35,6 +37,9 @@ impl<'a> WheelCache<'a> {
WheelCache::Path(url) => WheelCacheKind::Path
.root()
.join(digest(&CanonicalUrl::new(url))),
WheelCache::Editable(url) => WheelCacheKind::Editable
.root()
.join(digest(&CanonicalUrl::new(url))),
WheelCache::Git(url, sha) => WheelCacheKind::Git
.root()
.join(digest(&CanonicalUrl::new(url)))
@ -58,6 +63,8 @@ pub(crate) enum WheelCacheKind {
Url,
/// A cache of data from a local path.
Path,
/// A cache of data from an editable URL.
Editable,
/// A cache of data from a Git repository.
Git,
}
@ -69,6 +76,7 @@ impl WheelCacheKind {
Self::Index => "index",
Self::Url => "url",
Self::Path => "path",
Self::Editable => "editable",
Self::Git => "git",
}
}

View file

@ -199,7 +199,7 @@ impl<'a> BuildContext for BuildDispatch<'a> {
remote,
reinstalls,
extraneous: _,
} = Planner::with_requirements(&requirements).build(
} = Planner::new(&requirements).build(
site_packages,
&Reinstall::None,
&NoBinary::None,

View file

@ -11,16 +11,16 @@ use tempfile::TempDir;
use tokio::io::{AsyncRead, AsyncSeekExt, ReadBuf};
use tokio::sync::Semaphore;
use tokio_util::compat::FuturesAsyncReadCompatExt;
use tracing::{info_span, instrument, warn, Instrument};
use tracing::{debug, info_span, instrument, warn, Instrument};
use url::Url;
use distribution_filename::WheelFilename;
use distribution_types::{
BuildableSource, BuiltDist, Dist, FileLocation, HashPolicy, Hashed, IndexLocations,
LocalEditable, Name, SourceDist,
BuildableSource, BuiltDist, Dist, FileLocation, HashPolicy, Hashed, IndexLocations, Name,
SourceDist,
};
use platform_tags::Tags;
use pypi_types::{HashDigest, Metadata23};
use pypi_types::HashDigest;
use uv_cache::{ArchiveId, ArchiveTimestamp, CacheBucket, CacheEntry, Timestamp, WheelCache};
use uv_client::{
CacheControl, CachedClientError, Connectivity, DataWithCachePolicy, RegistryClient,
@ -133,32 +133,6 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> {
}
}
/// Build a directory into an editable wheel.
pub async fn build_wheel_editable(
&self,
editable: &LocalEditable,
editable_wheel_dir: &Path,
) -> Result<(LocalWheel, Metadata23), Error> {
// Build the wheel.
let (dist, disk_filename, filename, metadata) = self
.builder
.build_editable(editable, editable_wheel_dir)
.await?;
// Unzip into the editable wheel directory.
let path = editable_wheel_dir.join(&disk_filename);
let target = editable_wheel_dir.join(cache_key::digest(&editable.path));
let id = self.unzip_wheel(&path, &target).await?;
let wheel = LocalWheel {
dist,
filename,
archive: self.build_context.cache().archive(&id),
hashes: vec![],
};
Ok((wheel, metadata))
}
/// Fetch a wheel from the cache or download it from the index.
///
/// While hashes will be generated in all cases, hash-checking is _not_ enforced and should
@ -432,7 +406,11 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> {
// Optimization: Skip source dist download when we must not build them anyway.
if no_build {
return Err(Error::NoBuild);
if source.is_editable() {
debug!("Allowing build for editable source distribution: {source}");
} else {
return Err(Error::NoBuild);
}
}
let lock = self.locks.acquire(source).await;
@ -443,6 +421,7 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> {
.download_and_build_metadata(source, hashes, &self.client)
.boxed_local()
.await?;
Ok(metadata)
}

View file

@ -92,7 +92,11 @@ impl<'a> BuiltWheelIndex<'a> {
) -> Result<Option<CachedWheel>, Error> {
let cache_shard = self.cache.shard(
CacheBucket::BuiltWheels,
WheelCache::Path(&source_dist.url).root(),
if source_dist.editable {
WheelCache::Editable(&source_dist.url).root()
} else {
WheelCache::Path(&source_dist.url).root()
},
);
// Read the revision from the cache.

View file

@ -16,8 +16,8 @@ use zip::ZipArchive;
use distribution_filename::WheelFilename;
use distribution_types::{
BuildableSource, DirectorySourceDist, DirectorySourceUrl, Dist, FileLocation, GitSourceUrl,
HashPolicy, Hashed, LocalEditable, PathSourceUrl, RemoteSource, SourceDist, SourceUrl,
BuildableSource, DirectorySourceUrl, FileLocation, GitSourceUrl, HashPolicy, Hashed,
PathSourceUrl, RemoteSource, SourceDist, SourceUrl,
};
use install_wheel_rs::metadata::read_archive_metadata;
use platform_tags::Tags;
@ -369,7 +369,6 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
.boxed_local()
.await?
}
BuildableSource::Url(SourceUrl::Path(resource)) => {
let cache_shard = self.build_context.cache().shard(
CacheBucket::BuiltWheels,
@ -825,7 +824,8 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
Ok(revision)
}
/// Build a source distribution from a local source tree (i.e., directory).
/// Build a source distribution from a local source tree (i.e., directory), either editable or
/// non-editable.
async fn source_tree(
&self,
source: &BuildableSource<'_>,
@ -840,15 +840,17 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
let cache_shard = self.build_context.cache().shard(
CacheBucket::BuiltWheels,
WheelCache::Path(resource.url).root(),
if resource.editable {
WheelCache::Editable(resource.url).root()
} else {
WheelCache::Path(resource.url).root()
},
);
let _lock = lock_shard(&cache_shard).await?;
// Fetch the revision for the source distribution.
let revision = self
.source_tree_revision(source, resource, &cache_shard)
.await?;
let revision = self.source_tree_revision(resource, &cache_shard).await?;
// Scope all operations to the revision. Within the revision, there's no need to check for
// freshness, since entries have to be fresher than the revision itself.
@ -889,7 +891,8 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
})
}
/// Build the source distribution's metadata from a local source tree (i.e., a directory).
/// Build the source distribution's metadata from a local source tree (i.e., a directory),
/// either editable or non-editable.
///
/// If the build backend supports `prepare_metadata_for_build_wheel`, this method will avoid
/// building the wheel.
@ -906,15 +909,17 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
let cache_shard = self.build_context.cache().shard(
CacheBucket::BuiltWheels,
WheelCache::Path(resource.url).root(),
if resource.editable {
WheelCache::Editable(resource.url).root()
} else {
WheelCache::Path(resource.url).root()
},
);
let _lock = lock_shard(&cache_shard).await?;
// Fetch the revision for the source distribution.
let revision = self
.source_tree_revision(source, resource, &cache_shard)
.await?;
let revision = self.source_tree_revision(resource, &cache_shard).await?;
// Scope all operations to the revision. Within the revision, there's no need to check for
// freshness, since entries have to be fresher than the revision itself.
@ -971,7 +976,6 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
/// Return the [`Revision`] for a local source tree, refreshing it if necessary.
async fn source_tree_revision(
&self,
source: &BuildableSource<'_>,
resource: &DirectorySourceUrl<'_>,
cache_shard: &CacheShard,
) -> Result<Revision, Error> {
@ -982,16 +986,17 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
return Err(Error::DirWithoutEntrypoint(resource.path.to_path_buf()));
};
// Read the existing metadata from the cache.
// Read the existing metadata from the cache. We treat source trees as if `--refresh` is
// always set, since they're mutable.
let entry = cache_shard.entry(LOCAL_REVISION);
let freshness = self
let is_fresh = self
.build_context
.cache()
.freshness(&entry, source.name())
.is_fresh(&entry)
.map_err(Error::CacheRead)?;
// If the revision is fresh, return it.
if freshness.is_fresh() {
if is_fresh {
if let Some(pointer) = LocalRevisionPointer::read_from(&entry)? {
if pointer.timestamp == modified.timestamp() {
return Ok(pointer.into_revision());
@ -1299,8 +1304,13 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
source.name().is_some_and(|name| packages.contains(name))
}
};
if no_build {
return Err(Error::NoBuild);
if source.is_editable() {
debug!("Allowing build for editable source distribution: {source}");
} else {
return Err(Error::NoBuild);
}
}
// Build the wheel.
@ -1314,7 +1324,11 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
subdirectory,
&source.to_string(),
source.as_dist(),
BuildKind::Wheel,
if source.is_editable() {
BuildKind::Editable
} else {
BuildKind::Wheel
},
)
.await
.map_err(|err| Error::Build(source.to_string(), err))?
@ -1383,7 +1397,11 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
subdirectory,
&source.to_string(),
source.as_dist(),
BuildKind::Wheel,
if source.is_editable() {
BuildKind::Editable
} else {
BuildKind::Wheel
},
)
.await
.map_err(|err| Error::Build(source.to_string(), err))?;
@ -1410,49 +1428,6 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
Ok(Some(metadata))
}
/// Build a single directory into an editable wheel
pub async fn build_editable(
&self,
editable: &LocalEditable,
editable_wheel_dir: &Path,
) -> Result<(Dist, String, WheelFilename, Metadata23), Error> {
debug!("Building (editable) {editable}");
// Verify that the editable exists.
if !editable.path.exists() {
return Err(Error::NotFound(editable.path.clone()));
}
// Build the wheel.
let disk_filename = self
.build_context
.setup_build(
&editable.path,
None,
&editable.to_string(),
None,
BuildKind::Editable,
)
.await
.map_err(|err| Error::BuildEditable(editable.to_string(), err))?
.wheel(editable_wheel_dir)
.await
.map_err(|err| Error::BuildEditable(editable.to_string(), err))?;
let filename = WheelFilename::from_str(&disk_filename)?;
// We finally have the name of the package and can construct the dist.
let dist = Dist::Source(SourceDist::Directory(DirectorySourceDist {
name: filename.name.clone(),
url: editable.url().clone(),
path: editable.path.clone(),
editable: true,
}));
let metadata = read_wheel_metadata(&filename, editable_wheel_dir.join(&disk_filename))?;
debug!("Finished building (editable): {dist}");
Ok((dist, disk_filename, filename, metadata))
}
/// Returns a GET [`reqwest::Request`] for the given URL.
fn request(url: Url, client: &RegistryClient) -> Result<reqwest::Request, reqwest::Error> {
client

View file

@ -21,7 +21,6 @@ pep440_rs = { workspace = true }
pep508_rs = { workspace = true }
platform-tags = { workspace = true }
pypi-types = { workspace = true }
requirements-txt = { workspace = true }
uv-cache = { workspace = true }
uv-configuration = { workspace = true }
uv-distribution = { workspace = true }

View file

@ -1,24 +1,18 @@
use std::cmp::Reverse;
use std::path::Path;
use std::sync::Arc;
use futures::{stream::FuturesUnordered, FutureExt, Stream, StreamExt, TryFutureExt, TryStreamExt};
use futures::{stream::FuturesUnordered, FutureExt, Stream, TryFutureExt, TryStreamExt};
use pep508_rs::PackageName;
use tokio::task::JoinError;
use tracing::instrument;
use url::Url;
use distribution_types::{
BuildableSource, CachedDist, Dist, Hashed, Identifier, LocalEditable, LocalEditables,
RemoteSource,
};
use distribution_types::{BuildableSource, CachedDist, Dist, Hashed, Identifier, RemoteSource};
use platform_tags::Tags;
use uv_cache::Cache;
use uv_distribution::{DistributionDatabase, LocalWheel};
use uv_types::{BuildContext, HashStrategy, InFlight};
use crate::editable::BuiltEditable;
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Failed to unzip wheel: {0}")]
@ -115,55 +109,6 @@ impl<'a, Context: BuildContext> Downloader<'a, Context> {
Ok(wheels)
}
/// Build a set of editables
#[instrument(skip_all)]
pub async fn build_editables(
&self,
editables: LocalEditables,
editable_wheel_dir: &Path,
) -> Result<Vec<BuiltEditable>, Error> {
// Build editables in parallel
let mut results = Vec::with_capacity(editables.len());
let mut fetches = editables
.into_iter()
.map(|editable| async move {
let task_id = self
.reporter
.as_ref()
.map(|reporter| reporter.on_editable_build_start(&editable));
let (local_wheel, metadata) = self
.database
.build_wheel_editable(&editable, editable_wheel_dir)
.await
.map_err(Error::Editable)?;
let cached_dist = CachedDist::from(local_wheel);
if let Some(task_id) = task_id {
if let Some(reporter) = &self.reporter {
reporter.on_editable_build_complete(&editable, task_id);
}
}
Ok::<_, Error>((editable, cached_dist, metadata))
})
.collect::<FuturesUnordered<_>>();
while let Some((editable, wheel, metadata)) = fetches.next().await.transpose()? {
if let Some(reporter) = self.reporter.as_ref() {
reporter.on_progress(&wheel);
}
results.push(BuiltEditable {
editable,
wheel,
metadata,
});
}
if let Some(reporter) = self.reporter.as_ref() {
reporter.on_complete();
}
Ok(results)
}
/// Download, build, and unzip a single wheel.
#[instrument(skip_all, fields(name = % dist, size = ? dist.size(), url = dist.file().map(| file | file.url.to_string()).unwrap_or_default()))]
pub async fn get_wheel(&self, dist: Dist, in_flight: &InFlight) -> Result<CachedDist, Error> {
@ -241,12 +186,6 @@ pub trait Reporter: Send + Sync {
/// Callback to invoke when a source distribution build is complete.
fn on_build_complete(&self, source: &BuildableSource, id: usize);
/// Callback to invoke when a editable build is kicked off.
fn on_editable_build_start(&self, dist: &LocalEditable) -> usize;
/// Callback to invoke when a editable build is complete.
fn on_editable_build_complete(&self, dist: &LocalEditable, id: usize);
/// Callback to invoke when a repository checkout begins.
fn on_checkout_start(&self, url: &Url, rev: &str) -> usize;

View file

@ -1,158 +0,0 @@
use serde::Deserialize;
use std::path::Path;
use distribution_types::{
CachedDist, InstalledDist, InstalledMetadata, InstalledVersion, LocalEditable, Name,
};
use pypi_types::Metadata23;
use uv_normalize::PackageName;
/// An editable distribution that has been installed.
#[derive(Debug, Clone)]
pub struct InstalledEditable {
pub editable: LocalEditable,
pub wheel: InstalledDist,
pub metadata: Metadata23,
}
/// An editable distribution that has been built.
#[derive(Debug, Clone)]
pub struct BuiltEditable {
pub editable: LocalEditable,
pub wheel: CachedDist,
pub metadata: Metadata23,
}
/// An editable distribution that has been resolved to a concrete distribution.
#[derive(Debug, Clone)]
#[allow(clippy::large_enum_variant)]
pub enum ResolvedEditable {
/// The editable is already installed in the environment.
Installed(InstalledEditable),
/// The editable has been built and is ready to be installed.
Built(BuiltEditable),
}
impl ResolvedEditable {
/// Return the [`LocalEditable`] for the distribution.
pub fn local(&self) -> &LocalEditable {
match self {
Self::Installed(dist) => &dist.editable,
Self::Built(dist) => &dist.editable,
}
}
/// Return the [`Metadata23`] for the distribution.
pub fn metadata(&self) -> &Metadata23 {
match self {
Self::Installed(dist) => &dist.metadata,
Self::Built(dist) => &dist.metadata,
}
}
}
impl Name for InstalledEditable {
fn name(&self) -> &PackageName {
&self.metadata.name
}
}
impl Name for BuiltEditable {
fn name(&self) -> &PackageName {
&self.metadata.name
}
}
impl Name for ResolvedEditable {
fn name(&self) -> &PackageName {
match self {
Self::Installed(dist) => dist.name(),
Self::Built(dist) => dist.name(),
}
}
}
impl InstalledMetadata for InstalledEditable {
fn installed_version(&self) -> InstalledVersion {
self.wheel.installed_version()
}
}
impl InstalledMetadata for BuiltEditable {
fn installed_version(&self) -> InstalledVersion {
self.wheel.installed_version()
}
}
impl InstalledMetadata for ResolvedEditable {
fn installed_version(&self) -> InstalledVersion {
match self {
Self::Installed(dist) => dist.installed_version(),
Self::Built(dist) => dist.installed_version(),
}
}
}
impl std::fmt::Display for InstalledEditable {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}{}", self.name(), self.installed_version())
}
}
impl std::fmt::Display for BuiltEditable {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}{}", self.name(), self.installed_version())
}
}
impl std::fmt::Display for ResolvedEditable {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}{}", self.name(), self.installed_version())
}
}
/// Returns `true` if the source tree at the given path contains dynamic metadata.
pub fn is_dynamic(path: &Path) -> bool {
// If there's no `pyproject.toml`, we assume it's dynamic.
let Ok(contents) = fs_err::read_to_string(path.join("pyproject.toml")) else {
return true;
};
let Ok(pyproject_toml) = toml::from_str::<PyProjectToml>(&contents) else {
return true;
};
// If `[project]` is not present, we assume it's dynamic.
let Some(project) = pyproject_toml.project else {
// ...unless it appears to be a Poetry project.
return pyproject_toml
.tool
.map_or(true, |tool| tool.poetry.is_none());
};
// `[project.dynamic]` must be present and non-empty.
project.dynamic.is_some_and(|dynamic| !dynamic.is_empty())
}
/// A pyproject.toml as specified in PEP 517.
#[derive(Deserialize, Debug)]
#[serde(rename_all = "kebab-case")]
struct PyProjectToml {
project: Option<Project>,
tool: Option<Tool>,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "kebab-case")]
struct Project {
dynamic: Option<Vec<String>>,
}
#[derive(Deserialize, Debug)]
struct Tool {
poetry: Option<ToolPoetry>,
}
#[derive(Deserialize, Debug)]
struct ToolPoetry {
#[allow(dead_code)]
name: Option<String>,
}

View file

@ -1,6 +1,5 @@
pub use compile::{compile_tree, CompileError};
pub use downloader::{Downloader, Reporter as DownloadReporter};
pub use editable::{is_dynamic, BuiltEditable, InstalledEditable, ResolvedEditable};
pub use installer::{Installer, Reporter as InstallReporter};
pub use plan::{Plan, Planner};
pub use site_packages::{SatisfiesResult, SitePackages, SitePackagesDiagnostic};
@ -8,7 +7,7 @@ pub use uninstall::{uninstall, UninstallError};
mod compile;
mod downloader;
mod editable;
mod installer;
mod plan;
mod satisfies;

View file

@ -5,14 +5,13 @@ use std::str::FromStr;
use anyhow::{bail, Result};
use rustc_hash::FxHashMap;
use tracing::{debug, warn};
use tracing::debug;
use distribution_filename::WheelFilename;
use distribution_types::{
CachedDirectUrlDist, CachedDist, DirectUrlBuiltDist, DirectUrlSourceDist, DirectorySourceDist,
Error, GitSourceDist, Hashed, IndexLocations, InstalledDist, InstalledMetadata,
InstalledVersion, Name, PathBuiltDist, PathSourceDist, RemoteSource, Requirement,
RequirementSource, Verbatim,
Error, GitSourceDist, Hashed, IndexLocations, InstalledDist, Name, PathBuiltDist,
PathSourceDist, RemoteSource, Requirement, RequirementSource, Verbatim,
};
use platform_tags::Tags;
use uv_cache::{ArchiveTimestamp, Cache, CacheBucket, WheelCache};
@ -26,32 +25,18 @@ use uv_interpreter::PythonEnvironment;
use uv_types::HashStrategy;
use crate::satisfies::RequirementSatisfaction;
use crate::{ResolvedEditable, SitePackages};
use crate::SitePackages;
/// A planner to generate an [`Plan`] based on a set of requirements.
#[derive(Debug)]
pub struct Planner<'a> {
requirements: &'a [Requirement],
editable_requirements: &'a [ResolvedEditable],
}
impl<'a> Planner<'a> {
/// Set the requirements use in the [`Plan`].
#[must_use]
pub fn with_requirements(requirements: &'a [Requirement]) -> Self {
Self {
requirements,
editable_requirements: &[],
}
}
/// Set the editable requirements use in the [`Plan`].
#[must_use]
pub fn with_editable_requirements(self, editable_requirements: &'a [ResolvedEditable]) -> Self {
Self {
editable_requirements,
..self
}
pub fn new(requirements: &'a [Requirement]) -> Self {
Self { requirements }
}
/// Partition a set of requirements into those that should be linked from the cache, those that
@ -90,56 +75,6 @@ impl<'a> Planner<'a> {
BuildHasherDefault::default(),
);
// Remove any editable requirements.
for requirement in self.editable_requirements {
// If we see the same requirement twice, then we have a conflict.
let specifier = Specifier::Editable(requirement.installed_version());
match seen.entry(requirement.name().clone()) {
Entry::Occupied(value) => {
if value.get() == &specifier {
continue;
}
bail!(
"Detected duplicate package in requirements: {}",
requirement.name()
);
}
Entry::Vacant(entry) => {
entry.insert(specifier);
}
}
match requirement {
ResolvedEditable::Installed(installed) => {
debug!("Treating editable requirement as immutable: {installed}");
// Remove from the site-packages index, to avoid marking as extraneous.
let Some(editable) = installed.wheel.as_editable() else {
warn!("Editable requirement is not editable: {installed}");
continue;
};
let existing = site_packages.remove_editables(editable);
if existing.is_empty() {
warn!("Editable requirement is not installed: {installed}");
continue;
}
}
ResolvedEditable::Built(built) => {
debug!("Treating editable requirement as mutable: {built}");
// Remove any editables.
let existing = site_packages.remove_editables(built.editable.raw());
reinstalls.extend(existing);
// Remove any non-editable installs of the same package.
let existing = site_packages.remove_packages(built.name());
reinstalls.extend(existing);
cached.push(built.wheel.clone());
}
}
}
for requirement in self.requirements {
// Filter out incompatible requirements.
if !requirement.evaluate_markers(Some(venv.interpreter().markers()), &[]) {
@ -147,10 +82,9 @@ impl<'a> Planner<'a> {
}
// If we see the same requirement twice, then we have a conflict.
let specifier = Specifier::NonEditable(&requirement.source);
match seen.entry(requirement.name.clone()) {
Entry::Occupied(value) => {
if value.get() == &specifier {
if value.get() == &&requirement.source {
continue;
}
bail!(
@ -159,7 +93,7 @@ impl<'a> Planner<'a> {
);
}
Entry::Vacant(entry) => {
entry.insert(specifier);
entry.insert(&requirement.source);
}
}
@ -195,6 +129,9 @@ impl<'a> Planner<'a> {
RequirementSatisfaction::OutOfDate => {
debug!("Requirement installed, but not fresh: {distribution}");
}
RequirementSatisfaction::Dynamic => {
debug!("Requirement installed, but dynamic: {distribution}");
}
}
reinstalls.push(distribution.clone());
}
@ -332,7 +269,7 @@ impl<'a> Planner<'a> {
continue;
}
}
RequirementSource::Path { url, .. } => {
RequirementSource::Path { url, editable, .. } => {
// Store the canonicalized path, which also serves to validate that it exists.
let path = match url
.to_file_path()
@ -352,7 +289,7 @@ impl<'a> Planner<'a> {
name: requirement.name.clone(),
url: url.clone(),
path,
editable: false,
editable: *editable,
};
// Find the most-compatible wheel from the cache, since we don't know
@ -478,14 +415,6 @@ impl<'a> Planner<'a> {
}
}
#[derive(Debug, PartialEq, Eq)]
enum Specifier<'a> {
/// An editable requirement, marked by the installed version of the package.
Editable(InstalledVersion<'a>),
/// A non-editable requirement, marked by the version or URL specifier.
NonEditable(&'a RequirementSource),
}
#[derive(Debug, Default)]
pub struct Plan {
/// The distributions that are not already installed in the current environment, but are

View file

@ -1,8 +1,11 @@
use anyhow::Result;
use cache_key::{CanonicalUrl, RepositoryUrl};
use std::fmt::Debug;
use std::path::Path;
use anyhow::Result;
use serde::Deserialize;
use tracing::{debug, trace};
use cache_key::{CanonicalUrl, RepositoryUrl};
use distribution_types::{InstalledDirectUrlDist, InstalledDist, RequirementSource};
use pypi_types::{DirInfo, DirectUrl, VcsInfo, VcsKind};
use uv_cache::{ArchiveTarget, ArchiveTimestamp};
@ -12,6 +15,7 @@ pub(crate) enum RequirementSatisfaction {
Mismatch,
Satisfied,
OutOfDate,
Dynamic,
}
impl RequirementSatisfaction {
@ -187,9 +191,59 @@ impl RequirementSatisfaction {
return Ok(Self::OutOfDate);
}
// Does the package have dynamic metadata?
if is_dynamic(path) {
return Ok(Self::Dynamic);
}
// Otherwise, assume the requirement is up-to-date.
Ok(Self::Satisfied)
}
}
}
}
/// Returns `true` if the source tree at the given path contains dynamic metadata.
fn is_dynamic(path: &Path) -> bool {
// If there's no `pyproject.toml`, we assume it's dynamic.
let Ok(contents) = fs_err::read_to_string(path.join("pyproject.toml")) else {
return true;
};
let Ok(pyproject_toml) = toml::from_str::<PyProjectToml>(&contents) else {
return true;
};
// If `[project]` is not present, we assume it's dynamic.
let Some(project) = pyproject_toml.project else {
// ...unless it appears to be a Poetry project.
return pyproject_toml
.tool
.map_or(true, |tool| tool.poetry.is_none());
};
// `[project.dynamic]` must be present and non-empty.
project.dynamic.is_some_and(|dynamic| !dynamic.is_empty())
}
/// A pyproject.toml as specified in PEP 517.
#[derive(Deserialize, Debug)]
#[serde(rename_all = "kebab-case")]
struct PyProjectToml {
project: Option<Project>,
tool: Option<Tool>,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "kebab-case")]
struct Project {
dynamic: Option<Vec<String>>,
}
#[derive(Deserialize, Debug)]
struct Tool {
poetry: Option<ToolPoetry>,
}
#[derive(Deserialize, Debug)]
struct ToolPoetry {
#[allow(dead_code)]
name: Option<String>,
}

View file

@ -13,13 +13,10 @@ use distribution_types::{
};
use pep440_rs::{Version, VersionSpecifiers};
use pypi_types::VerbatimParsedUrl;
use requirements_txt::EditableRequirement;
use uv_cache::{ArchiveTarget, ArchiveTimestamp};
use uv_interpreter::PythonEnvironment;
use uv_normalize::PackageName;
use uv_types::InstalledPackagesProvider;
use crate::is_dynamic;
use crate::satisfies::RequirementSatisfaction;
/// An index over the packages installed in an environment.
@ -289,7 +286,6 @@ impl SitePackages {
pub fn satisfies(
&self,
requirements: &[UnresolvedRequirementSpecification],
editables: &[EditableRequirement],
constraints: &[Requirement],
) -> Result<SatisfiesResult> {
let mut stack = Vec::with_capacity(requirements.len());
@ -308,58 +304,6 @@ impl SitePackages {
}
}
// Verify that all editable requirements are met.
for requirement in editables {
let installed = self.get_editables(requirement.raw());
match installed.as_slice() {
[] => {
// The package isn't installed.
return Ok(SatisfiesResult::Unsatisfied(requirement.to_string()));
}
[distribution] => {
// Is the editable out-of-date?
if !ArchiveTimestamp::up_to_date_with(
&requirement.path,
ArchiveTarget::Install(distribution),
)? {
return Ok(SatisfiesResult::Unsatisfied(requirement.to_string()));
}
// Does the editable have dynamic metadata?
if is_dynamic(&requirement.path) {
return Ok(SatisfiesResult::Unsatisfied(requirement.to_string()));
}
// Recurse into the dependencies.
let metadata = distribution
.metadata()
.with_context(|| format!("Failed to read metadata for: {distribution}"))?;
// Add the dependencies to the queue.
for dependency in metadata.requires_dist {
if dependency.evaluate_markers(
self.venv.interpreter().markers(),
&requirement.extras,
) {
let dependency = UnresolvedRequirementSpecification {
requirement: UnresolvedRequirement::Named(Requirement::from(
dependency,
)),
hashes: vec![],
};
if seen.insert(dependency.clone()) {
stack.push(dependency);
}
}
}
}
_ => {
// There are multiple installed distributions for the same package.
return Ok(SatisfiesResult::Unsatisfied(requirement.to_string()));
}
}
}
// Verify that all non-editable requirements are met.
while let Some(entry) = stack.pop() {
let installed = match &entry.requirement {
@ -378,7 +322,9 @@ impl SitePackages {
distribution,
entry.requirement.source().as_ref(),
)? {
RequirementSatisfaction::Mismatch | RequirementSatisfaction::OutOfDate => {
RequirementSatisfaction::Mismatch
| RequirementSatisfaction::OutOfDate
| RequirementSatisfaction::Dynamic => {
return Ok(SatisfiesResult::Unsatisfied(entry.requirement.to_string()))
}
RequirementSatisfaction::Satisfied => {}
@ -387,7 +333,8 @@ impl SitePackages {
for constraint in constraints {
match RequirementSatisfaction::check(distribution, &constraint.source)? {
RequirementSatisfaction::Mismatch
| RequirementSatisfaction::OutOfDate => {
| RequirementSatisfaction::OutOfDate
| RequirementSatisfaction::Dynamic => {
return Ok(SatisfiesResult::Unsatisfied(
entry.requirement.to_string(),
))

View file

@ -16,7 +16,7 @@ distribution-types = { workspace = true }
pep440_rs = { workspace = true }
pep508_rs = { workspace = true }
pypi-types = { workspace = true }
requirements-txt = { workspace = true, features = ["reqwest"] }
requirements-txt = { workspace = true, features = ["http"] }
uv-client = { workspace = true }
uv-configuration = { workspace = true }
uv-distribution = { workspace = true }

View file

@ -14,7 +14,7 @@ use pep508_rs::MarkerEnvironment;
use uv_configuration::{Constraints, Overrides};
use uv_distribution::{DistributionDatabase, Reporter};
use uv_git::GitUrl;
use uv_resolver::{BuiltEditableMetadata, InMemoryIndex, MetadataResponse};
use uv_resolver::{InMemoryIndex, MetadataResponse};
use uv_types::{BuildContext, HashStrategy, RequestedRequirements};
#[derive(Debug, Error)]
@ -50,8 +50,6 @@ pub struct LookaheadResolver<'a, Context: BuildContext> {
constraints: &'a Constraints,
/// The overrides for the project.
overrides: &'a Overrides,
/// The editable requirements for the project.
editables: &'a [BuiltEditableMetadata],
/// The required hashes for the project.
hasher: &'a HashStrategy,
/// The in-memory index for resolving dependencies.
@ -67,7 +65,6 @@ impl<'a, Context: BuildContext> LookaheadResolver<'a, Context> {
requirements: &'a [Requirement],
constraints: &'a Constraints,
overrides: &'a Overrides,
editables: &'a [BuiltEditableMetadata],
hasher: &'a HashStrategy,
index: &'a InMemoryIndex,
database: DistributionDatabase<'a, Context>,
@ -76,7 +73,6 @@ impl<'a, Context: BuildContext> LookaheadResolver<'a, Context> {
requirements,
constraints,
overrides,
editables,
hasher,
index,
database,
@ -111,13 +107,6 @@ impl<'a, Context: BuildContext> LookaheadResolver<'a, Context> {
.constraints
.apply(self.overrides.apply(self.requirements))
.filter(|requirement| requirement.evaluate_markers(markers, &[]))
.chain(self.editables.iter().flat_map(|editable| {
self.constraints
.apply(self.overrides.apply(&editable.requirements.dependencies))
.filter(|requirement| {
requirement.evaluate_markers(markers, &editable.built.extras)
})
}))
.cloned()
.collect();
@ -184,9 +173,8 @@ impl<'a, Context: BuildContext> LookaheadResolver<'a, Context> {
RequirementSource::Path {
path,
url,
// TODO(konsti): Figure out why we lose the editable here (does it matter?)
editable: _,
} => Dist::from_file_url(requirement.name, url, &path, false)?,
editable,
} => Dist::from_file_url(requirement.name, url, &path, editable)?,
};
// Fetch the metadata for the distribution.

View file

@ -104,6 +104,7 @@ impl<'a, Context: BuildContext> SourceTreeResolver<'a, Context> {
let source = SourceUrl::Directory(DirectorySourceUrl {
url: &url,
path: Cow::Borrowed(source_tree),
editable: false,
});
// Determine the hash policy. Since we don't have a package name, we perform a

View file

@ -28,7 +28,6 @@
//! `source_trees`.
use std::collections::VecDeque;
use std::iter;
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
@ -66,8 +65,6 @@ pub struct RequirementsSpecification {
pub constraints: Vec<Requirement>,
/// The overrides for the project.
pub overrides: Vec<UnresolvedRequirementSpecification>,
/// Package to install as editable installs
pub editables: Vec<EditableRequirement>,
/// The source trees from which to extract requirements.
pub source_trees: Vec<PathBuf>,
/// The extras used to collect requirements.
@ -121,13 +118,18 @@ impl RequirementsSpecification {
.requirements
.into_iter()
.map(UnresolvedRequirementSpecification::from)
.chain(
requirements_txt
.editables
.into_iter()
.map(UnresolvedRequirementSpecification::from),
)
.collect(),
constraints: requirements_txt
.constraints
.into_iter()
.map(Requirement::from)
.collect(),
editables: requirements_txt.editables,
index_url: requirements_txt.index_url.map(IndexUrl::from),
extra_index_urls: requirements_txt
.extra_index_urls
@ -191,6 +193,7 @@ impl RequirementsSpecification {
.find(|member| is_same_file(member.root(), &requirement.path).unwrap_or(false))
.map(|member| (member.pyproject_toml(), workspace))
});
let editable_spec = if let Some((pyproject_toml, workspace)) = project_in_exiting_workspace
{
Self::parse_direct_pyproject_toml(
@ -222,22 +225,23 @@ impl RequirementsSpecification {
requirement.path.user_display()
);
return Ok(Self {
editables: vec![requirement],
requirements: vec![UnresolvedRequirementSpecification::from(requirement)],
..Self::default()
});
};
if let Some(editable_spec) = editable_spec {
// We only collect the editables here to keep the count of root packages
// correct.
// We only collect the editables here to keep the count of root packages correct.
// TODO(konsti): Collect all workspace packages, even the non-editable ones.
let editables = editable_spec
.editables
.into_iter()
.chain(iter::once(requirement))
.collect();
Ok(Self {
editables,
requirements: editable_spec
.requirements
.into_iter()
.chain(std::iter::once(UnresolvedRequirementSpecification::from(
requirement,
)))
.filter(|entry| entry.requirement.is_editable())
.collect(),
..Self::default()
})
} else {
@ -246,7 +250,7 @@ impl RequirementsSpecification {
requirement.path.user_display()
);
Ok(Self {
editables: vec![requirement],
requirements: vec![UnresolvedRequirementSpecification::from(requirement)],
..Self::default()
})
}
@ -361,14 +365,13 @@ impl RequirementsSpecification {
preview: PreviewMode,
project: Pep621Metadata,
) -> Result<RequirementsSpecification> {
let mut seen_editables = FxHashSet::from_iter([project.name.clone()]);
let mut seen = FxHashSet::from_iter([project.name.clone()]);
let mut queue = VecDeque::from([project.name.clone()]);
let mut editables = Vec::new();
let mut requirements = Vec::new();
let mut used_extras = FxHashSet::default();
while let Some(project_name) = queue.pop_front() {
let Some(current) = &workspace.packages().get(&project_name) else {
let Some(current) = workspace.packages().get(&project_name) else {
continue;
};
trace!("Processing metadata for workspace package {project_name}");
@ -396,40 +399,30 @@ impl RequirementsSpecification {
current.root().user_display()
)
})?;
used_extras.extend(project.used_extras);
// Partition into editable and non-editable requirements.
for requirement in project.requirements {
if let RequirementSource::Path {
path,
editable: true,
url,
} = requirement.source
{
editables.push(EditableRequirement {
url,
path,
marker: requirement.marker,
extras: requirement.extras,
origin: requirement.origin,
});
if seen_editables.insert(requirement.name.clone()) {
// Recurse into any editables.
for requirement in &project.requirements {
if matches!(
requirement.source,
RequirementSource::Path { editable: true, .. }
) {
if seen.insert(requirement.name.clone()) {
queue.push_back(requirement.name.clone());
}
} else {
requirements.push(UnresolvedRequirementSpecification {
requirement: UnresolvedRequirement::Named(requirement),
hashes: vec![],
});
}
}
// Collect the requirements and extras.
used_extras.extend(project.used_extras);
requirements.extend(project.requirements);
}
let spec = Self {
project: Some(project.name),
editables,
requirements,
requirements: requirements
.into_iter()
.map(UnresolvedRequirementSpecification::from)
.collect(),
extras: used_extras,
..Self::default()
};
@ -459,7 +452,6 @@ impl RequirementsSpecification {
spec.constraints.extend(source.constraints);
spec.overrides.extend(source.overrides);
spec.extras.extend(source.extras);
spec.editables.extend(source.editables);
spec.source_trees.extend(source.source_trees);
// Use the first project name discovered.

View file

@ -224,6 +224,7 @@ impl<'a, Context: BuildContext> NamedRequirementsResolver<'a, Context> {
SourceUrl::Directory(DirectorySourceUrl {
url: &requirement.url.verbatim,
path: Cow::Borrowed(&parsed_path_url.path),
editable: parsed_path_url.editable,
})
}
// If it's not a directory, assume it's a file.

View file

@ -1,44 +0,0 @@
use rustc_hash::FxHashMap;
use distribution_types::{LocalEditable, Requirements};
use pypi_types::Metadata23;
use uv_normalize::PackageName;
/// A built editable for which we know its dependencies and other static metadata.
#[derive(Debug, Clone)]
pub struct BuiltEditableMetadata {
pub built: LocalEditable,
pub metadata: Metadata23,
pub requirements: Requirements,
}
/// A set of editable packages, indexed by package name.
#[derive(Debug, Default, Clone)]
pub(crate) struct Editables(FxHashMap<PackageName, BuiltEditableMetadata>);
impl Editables {
/// Create a new set of editables from a set of requirements.
pub(crate) fn from_requirements(requirements: Vec<BuiltEditableMetadata>) -> Self {
Self(
requirements
.into_iter()
.map(|editable| (editable.metadata.name.clone(), editable))
.collect(),
)
}
/// Get the editable for a package.
pub(crate) fn get(&self, name: &PackageName) -> Option<&BuiltEditableMetadata> {
self.0.get(name)
}
/// Returns `true` if the given package is editable.
pub(crate) fn contains(&self, name: &PackageName) -> bool {
self.0.contains_key(name)
}
/// Iterate over all editables.
pub(crate) fn iter(&self) -> impl Iterator<Item = &BuiltEditableMetadata> {
self.0.values()
}
}

View file

@ -1,5 +1,4 @@
pub use dependency_mode::DependencyMode;
pub use editables::BuiltEditableMetadata;
pub use error::ResolveError;
pub use exclude_newer::ExcludeNewer;
pub use exclusions::Exclusions;
@ -25,7 +24,6 @@ mod candidate_selector;
mod dependency_mode;
mod dependency_provider;
mod editables;
mod error;
mod exclude_newer;
mod exclusions;

View file

@ -6,7 +6,6 @@ use uv_configuration::{Constraints, Overrides};
use uv_normalize::PackageName;
use uv_types::RequestedRequirements;
use crate::editables::BuiltEditableMetadata;
use crate::{preferences::Preference, DependencyMode, Exclusions};
/// A manifest of requirements, constraints, and preferences.
@ -31,12 +30,6 @@ pub struct Manifest {
/// The name of the project.
pub(crate) project: Option<PackageName>,
/// The editable requirements for the project, which are built in advance.
///
/// The requirements of the editables should be included in resolution as if they were
/// direct requirements in their own right.
pub(crate) editables: Vec<BuiltEditableMetadata>,
/// The installed packages to exclude from consideration during resolution.
///
/// These typically represent packages that are being upgraded or reinstalled
@ -59,7 +52,6 @@ impl Manifest {
overrides: Overrides,
preferences: Vec<Preference>,
project: Option<PackageName>,
editables: Vec<BuiltEditableMetadata>,
exclusions: Exclusions,
lookaheads: Vec<RequestedRequirements>,
) -> Self {
@ -69,7 +61,6 @@ impl Manifest {
overrides,
preferences,
project,
editables,
exclusions,
lookaheads,
}
@ -82,7 +73,6 @@ impl Manifest {
overrides: Overrides::default(),
preferences: Vec::new(),
project: None,
editables: Vec::new(),
exclusions: Exclusions::default(),
lookaheads: Vec::new(),
}
@ -113,13 +103,6 @@ impl Manifest {
requirement.evaluate_markers(markers, lookahead.extras())
})
})
.chain(self.editables.iter().flat_map(move |editable| {
self.overrides
.apply(&editable.requirements.dependencies)
.filter(move |requirement| {
requirement.evaluate_markers(markers, &editable.built.extras)
})
}))
.chain(
self.overrides
.apply(&self.requirements)
@ -175,13 +158,6 @@ impl Manifest {
requirement.evaluate_markers(markers, lookahead.extras())
})
})
.chain(self.editables.iter().flat_map(move |editable| {
self.overrides
.apply(&editable.requirements.dependencies)
.filter(move |requirement| {
requirement.evaluate_markers(markers, &editable.built.extras)
})
}))
.chain(
self.overrides
.apply(&self.requirements)
@ -213,6 +189,6 @@ impl Manifest {
/// Returns the number of input requirements.
pub fn num_requirements(&self) -> usize {
self.requirements.len() + self.editables.len()
self.requirements.len()
}
}

View file

@ -49,11 +49,6 @@ impl PubGrubDependencies {
Ok(Self(dependencies))
}
/// Add a [`PubGrubPackage`] and [`PubGrubVersion`] range into the dependencies.
pub(crate) fn push(&mut self, package: PubGrubPackage, version: Range<Version>) {
self.0.push((package, version));
}
/// Iterate over the dependencies.
pub(crate) fn iter(&self) -> impl Iterator<Item = &(PubGrubPackage, Range<Version>)> {
self.0.iter()

View file

@ -1,4 +1,4 @@
pub(crate) use crate::pubgrub::dependencies::{PubGrubDependencies, PubGrubRequirement};
pub(crate) use crate::pubgrub::dependencies::PubGrubDependencies;
pub(crate) use crate::pubgrub::distribution::PubGrubDistribution;
pub(crate) use crate::pubgrub::package::{PubGrubPackage, PubGrubPackageInner, PubGrubPython};
pub(crate) use crate::pubgrub::priority::{PubGrubPriorities, PubGrubPriority};

View file

@ -1,15 +1,12 @@
use std::borrow::Cow;
use std::collections::BTreeSet;
use owo_colors::OwoColorize;
use petgraph::visit::EdgeRef;
use petgraph::Direction;
use distribution_types::{IndexUrl, LocalEditable, Name, SourceAnnotations, Verbatim};
use pypi_types::HashDigest;
use distribution_types::{Name, SourceAnnotations};
use uv_normalize::PackageName;
use crate::resolution::AnnotatedDist;
use crate::ResolutionGraph;
/// A [`std::fmt::Display`] implementation for the resolution graph.
@ -77,48 +74,6 @@ impl<'a> DisplayResolutionGraph<'a> {
}
}
#[derive(Debug)]
enum Node<'a> {
/// A node linked to an editable distribution.
Editable(&'a LocalEditable),
/// A node linked to a non-editable distribution.
Distribution(&'a AnnotatedDist),
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
enum NodeKey<'a> {
/// A node linked to an editable distribution, sorted by verbatim representation.
Editable(Cow<'a, str>),
/// A node linked to a non-editable distribution, sorted by package name.
Distribution(&'a PackageName),
}
impl<'a> Node<'a> {
/// Return a comparable key for the node.
fn key(&self) -> NodeKey<'a> {
match self {
Node::Editable(editable) => NodeKey::Editable(editable.verbatim()),
Node::Distribution(annotated) => NodeKey::Distribution(annotated.name()),
}
}
/// Return the [`IndexUrl`] of the distribution, if any.
fn index(&self) -> Option<&IndexUrl> {
match self {
Node::Editable(_) => None,
Node::Distribution(annotated) => annotated.dist.index(),
}
}
/// Return the hashes of the distribution.
fn hashes(&self) -> &[HashDigest] {
match self {
Node::Editable(_) => &[],
Node::Distribution(annotated) => &annotated.hashes,
}
}
}
/// Write the graph in the `{name}=={version}` format of requirements.txt that pip uses.
impl std::fmt::Display for DisplayResolutionGraph<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
@ -134,32 +89,22 @@ impl std::fmt::Display for DisplayResolutionGraph<'_> {
return None;
}
let node = if let Some(editable) = self.resolution.editables.get(name) {
Node::Editable(&editable.built)
} else {
Node::Distribution(dist)
};
Some((index, node))
Some((index, dist))
})
.collect::<Vec<_>>();
// Sort the nodes by name, but with editable packages first.
nodes.sort_unstable_by_key(|(index, node)| (node.key(), *index));
nodes.sort_unstable_by_key(|(index, node)| (node.to_comparator(), *index));
// Print out the dependency graph.
for (index, node) in nodes {
// Display the node itself.
let mut line = match node {
Node::Editable(editable) => format!("-e {}", editable.verbatim()),
Node::Distribution(dist) => {
dist.to_requirements_txt(self.include_extras).to_string()
}
};
let mut line = node.to_requirements_txt(self.include_extras).to_string();
// Display the distribution hashes, if any.
let mut has_hashes = false;
if self.show_hashes {
for hash in node.hashes() {
for hash in &node.hashes {
has_hashes = true;
line.push_str(" \\\n");
line.push_str(" --hash=");
@ -184,12 +129,7 @@ impl std::fmt::Display for DisplayResolutionGraph<'_> {
// Include all external sources (e.g., requirements files).
let default = BTreeSet::default();
let source = match node {
Node::Editable(editable) => {
self.sources.get_editable(&editable.url).unwrap_or(&default)
}
Node::Distribution(dist) => self.sources.get(dist.name()).unwrap_or(&default),
};
let source = self.sources.get(node.name()).unwrap_or(&default);
match self.annotation_style {
AnnotationStyle::Line => match edges.as_slice() {
@ -261,7 +201,7 @@ impl std::fmt::Display for DisplayResolutionGraph<'_> {
// If enabled, include indexes to indicate which index was used for each package (e.g.,
// `# from https://pypi.org/simple`).
if self.include_index_annotation {
if let Some(index) = node.index() {
if let Some(index) = node.dist.index() {
let url = index.redacted();
writeln!(f, "{}", format!(" # from {url}").green())?;
}

View file

@ -16,7 +16,6 @@ use pypi_types::{ParsedUrlError, Yanked};
use uv_normalize::PackageName;
use crate::dependency_provider::UvDependencyProvider;
use crate::editables::Editables;
use crate::pins::FilePins;
use crate::preferences::Preferences;
use crate::pubgrub::{PubGrubDistribution, PubGrubPackageInner};
@ -34,8 +33,6 @@ use crate::{
pub struct ResolutionGraph {
/// The underlying graph.
pub(crate) petgraph: petgraph::graph::Graph<AnnotatedDist, Range<Version>, petgraph::Directed>,
/// The set of editable requirements in this resolution.
pub(crate) editables: Editables,
/// Any diagnostics that were encountered while building the graph.
pub(crate) diagnostics: Vec<ResolutionDiagnostic>,
}
@ -50,7 +47,6 @@ impl ResolutionGraph {
distributions: &FxOnceMap<VersionId, Arc<MetadataResponse>>,
state: &State<UvDependencyProvider>,
preferences: &Preferences,
editables: Editables,
) -> anyhow::Result<Self, ResolveError> {
// Collect and validate the extras.
let mut extras = FxHashMap::default();
@ -102,50 +98,34 @@ impl ResolutionGraph {
marker: None,
url: Some(url),
} => {
if let Some(editable) = editables.get(name) {
if editable.metadata.provides_extras.contains(extra) {
extras
.entry(name.clone())
.or_insert_with(Vec::new)
.push(extra.clone());
} else {
let dist = Dist::from_editable(name.clone(), editable.built.clone())?;
let dist = PubGrubDistribution::from_url(name, url);
diagnostics.push(ResolutionDiagnostic::MissingExtra {
dist: dist.into(),
extra: extra.clone(),
});
}
let response = distributions.get(&dist.version_id()).unwrap_or_else(|| {
panic!(
"Every package should have metadata: {:?}",
dist.version_id()
)
});
let MetadataResponse::Found(archive) = &*response else {
panic!(
"Every package should have metadata: {:?}",
dist.version_id()
)
};
if archive.metadata.provides_extras.contains(extra) {
extras
.entry(name.clone())
.or_insert_with(Vec::new)
.push(extra.clone());
} else {
let dist = PubGrubDistribution::from_url(name, url);
let dist = Dist::from_url(name.clone(), url_to_precise(url.clone()))?;
let response = distributions.get(&dist.version_id()).unwrap_or_else(|| {
panic!(
"Every package should have metadata: {:?}",
dist.version_id()
)
diagnostics.push(ResolutionDiagnostic::MissingExtra {
dist: dist.into(),
extra: extra.clone(),
});
let MetadataResponse::Found(archive) = &*response else {
panic!(
"Every package should have metadata: {:?}",
dist.version_id()
)
};
if archive.metadata.provides_extras.contains(extra) {
extras
.entry(name.clone())
.or_insert_with(Vec::new)
.push(extra.clone());
} else {
let dist = Dist::from_url(name.clone(), url_to_precise(url.clone()))?;
diagnostics.push(ResolutionDiagnostic::MissingExtra {
dist: dist.into(),
extra: extra.clone(),
});
}
}
}
_ => {}
@ -254,75 +234,60 @@ impl ResolutionGraph {
url: Some(url),
} => {
// Create the distribution.
if let Some(editable) = editables.get(name) {
let dist = Dist::from_editable(name.clone(), editable.built.clone())?;
// Add the distribution to the graph.
let index = petgraph.add_node(AnnotatedDist {
dist: dist.into(),
extras: editable.built.extras.clone(),
hashes: vec![],
metadata: editable.metadata.clone(),
});
inverse.insert(name, index);
} else {
let dist = Dist::from_url(name.clone(), url_to_precise(url.clone()))?;
let dist = Dist::from_url(name.clone(), url_to_precise(url.clone()))?;
// Extract the hashes, preserving those that were already present in the
// lockfile if necessary.
let hashes = if let Some(digests) = preferences
.match_hashes(name, version)
.filter(|digests| !digests.is_empty())
{
digests.to_vec()
} else if let Some(metadata_response) =
distributions.get(&dist.version_id())
{
if let MetadataResponse::Found(ref archive) = *metadata_response {
let mut digests = archive.hashes.clone();
digests.sort_unstable();
digests
} else {
vec![]
}
// Extract the hashes, preserving those that were already present in the
// lockfile if necessary.
let hashes = if let Some(digests) = preferences
.match_hashes(name, version)
.filter(|digests| !digests.is_empty())
{
digests.to_vec()
} else if let Some(metadata_response) = distributions.get(&dist.version_id()) {
if let MetadataResponse::Found(ref archive) = *metadata_response {
let mut digests = archive.hashes.clone();
digests.sort_unstable();
digests
} else {
vec![]
};
// Extract the metadata.
let metadata = {
let dist = PubGrubDistribution::from_url(name, url);
let response =
distributions.get(&dist.version_id()).unwrap_or_else(|| {
panic!(
"Every package should have metadata: {:?}",
dist.version_id()
)
});
let MetadataResponse::Found(archive) = &*response else {
panic!(
"Every package should have metadata: {:?}",
dist.version_id()
)
};
archive.metadata.clone()
};
// Extract the extras.
let extras = extras.get(name).cloned().unwrap_or_default();
// Add the distribution to the graph.
let index = petgraph.add_node(AnnotatedDist {
dist: dist.into(),
extras,
hashes,
metadata,
});
inverse.insert(name, index);
}
} else {
vec![]
};
// Extract the metadata.
let metadata = {
let dist = PubGrubDistribution::from_url(name, url);
let response = distributions.get(&dist.version_id()).unwrap_or_else(|| {
panic!(
"Every package should have metadata: {:?}",
dist.version_id()
)
});
let MetadataResponse::Found(archive) = &*response else {
panic!(
"Every package should have metadata: {:?}",
dist.version_id()
)
};
archive.metadata.clone()
};
// Extract the extras.
let extras = extras.get(name).cloned().unwrap_or_default();
// Add the distribution to the graph.
let index = petgraph.add_node(AnnotatedDist {
dist: dist.into(),
extras,
hashes,
metadata,
});
inverse.insert(name, index);
}
_ => {}
};
@ -380,7 +345,6 @@ impl ResolutionGraph {
Ok(Self {
petgraph,
editables,
diagnostics,
})
}
@ -523,13 +487,7 @@ impl ResolutionGraph {
}
// Ensure that we consider markers from direct dependencies.
let direct_reqs = manifest.requirements.iter().chain(
manifest
.editables
.iter()
.flat_map(|editable| &editable.requirements.dependencies),
);
for direct_req in manifest.apply(direct_reqs) {
for direct_req in manifest.apply(manifest.requirements.iter()) {
let Some(ref marker_tree) = direct_req.marker else {
continue;
};

View file

@ -34,6 +34,14 @@ impl AnnotatedDist {
/// unnamed requirement for relative paths, which can't be represented with PEP 508 (but are
/// supported in `requirements.txt`).
pub(crate) fn to_requirements_txt(&self, include_extras: bool) -> Cow<str> {
// If the URL is editable, write it as an editable requirement.
if self.dist.is_editable() {
if let VersionOrUrlRef::Url(url) = self.dist.version_or_url() {
let given = url.verbatim();
return Cow::Owned(format!("-e {given}"));
}
}
// If the URL is not _definitively_ an absolute `file://` URL, write it as a relative path.
if self.dist.is_local() {
if let VersionOrUrlRef::Url(url) = self.dist.version_or_url() {
@ -94,6 +102,22 @@ impl AnnotatedDist {
))
}
}
pub(crate) fn to_comparator(&self) -> RequirementsTxtComparator {
if self.dist.is_editable() {
if let VersionOrUrlRef::Url(url) = self.dist.version_or_url() {
return RequirementsTxtComparator::Url(url.verbatim());
}
}
RequirementsTxtComparator::Name(self.name())
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) enum RequirementsTxtComparator<'a> {
Url(Cow<'a, str>),
Name(&'a PackageName),
}
impl Name for AnnotatedDist {

View file

@ -37,14 +37,13 @@ use uv_types::{BuildContext, HashStrategy, InstalledPackagesProvider};
use crate::candidate_selector::{CandidateDist, CandidateSelector};
use crate::dependency_provider::UvDependencyProvider;
use crate::editables::Editables;
use crate::error::ResolveError;
use crate::manifest::Manifest;
use crate::pins::FilePins;
use crate::preferences::Preferences;
use crate::pubgrub::{
PubGrubDependencies, PubGrubDistribution, PubGrubPackage, PubGrubPackageInner,
PubGrubPriorities, PubGrubPython, PubGrubRequirement, PubGrubSpecifier,
PubGrubPriorities, PubGrubPython, PubGrubSpecifier,
};
use crate::python_requirement::PythonRequirement;
use crate::resolution::ResolutionGraph;
@ -85,7 +84,6 @@ struct ResolverState<InstalledPackages: InstalledPackagesProvider> {
overrides: Overrides,
preferences: Preferences,
exclusions: Exclusions,
editables: Editables,
urls: Urls,
locals: Locals,
dependency_mode: DependencyMode,
@ -192,7 +190,6 @@ impl<Provider: ResolverProvider, InstalledPackages: InstalledPackagesProvider>
overrides: manifest.overrides,
preferences: Preferences::from_iter(manifest.preferences, markers),
exclusions: manifest.exclusions,
editables: Editables::from_requirements(manifest.editables),
hasher: hasher.clone(),
markers: markers.cloned(),
python_requirement: python_requirement.clone(),
@ -343,7 +340,6 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
self.index.distributions(),
&state.pubgrub,
&self.preferences,
self.editables.clone(),
);
};
state.next = highest_priority_pkg;
@ -560,11 +556,6 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
return Err(ResolveError::UnhashedPackage(name.clone()));
}
// If the package is an editable, we don't need to fetch metadata.
if self.editables.contains(name) {
return Ok(());
}
// Emit a request to fetch the metadata for this distribution.
let dist = Dist::from_url(name.clone(), url.clone())?;
if self.index.distributions().register(dist.version_id()) {
@ -649,31 +640,6 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
url.verbatim
);
// If the dist is an editable, return the version from the editable metadata.
if let Some(editable) = self.editables.get(name) {
let version = &editable.metadata.version;
// The version is incompatible with the requirement.
if !range.contains(version) {
return Ok(None);
}
// The version is incompatible due to its Python requirement.
if let Some(requires_python) = editable.metadata.requires_python.as_ref() {
let target = self.python_requirement.target();
if !requires_python.contains(target) {
return Ok(Some(ResolverVersion::Unavailable(
version.clone(),
UnavailableVersion::IncompatibleDist(IncompatibleDist::Source(
IncompatibleSource::RequiresPython(requires_python.clone()),
)),
)));
}
}
return Ok(Some(ResolverVersion::Available(version.clone())));
}
let dist = PubGrubDistribution::from_url(name, url);
let response = self
.index
@ -853,7 +819,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
self.markers.as_ref(),
);
let mut dependencies = match dependencies {
let dependencies = match dependencies {
Ok(dependencies) => dependencies,
Err(err) => {
return Ok(Dependencies::Unavailable(
@ -872,54 +838,6 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
self.visit_package(package, request_sink)?;
}
// Add a dependency on each editable.
for editable in self.editables.iter() {
let package = PubGrubPackage::from_package(
editable.metadata.name.clone(),
None,
None,
&self.urls,
);
let version = Range::singleton(editable.metadata.version.clone());
// Update the package priorities.
priorities.insert(&package, &version);
// Add the editable as a direct dependency.
dependencies.push(package, version);
// Add a dependency on each extra.
for extra in &editable.built.extras {
dependencies.push(
PubGrubPackage::from_package(
editable.metadata.name.clone(),
Some(extra.clone()),
None,
&self.urls,
),
Range::singleton(editable.metadata.version.clone()),
);
}
// Add any constraints.
for constraint in self
.constraints
.get(&editable.metadata.name)
.into_iter()
.flatten()
{
if constraint.evaluate_markers(self.markers.as_ref(), &[]) {
let PubGrubRequirement { package, version } =
PubGrubRequirement::from_constraint(
constraint,
&self.urls,
&self.locals,
)?;
dependencies.push(package, version);
}
}
}
Ok(Dependencies::Available(dependencies.into()))
}
@ -935,57 +853,22 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
if self.dependency_mode.is_direct() {
// If an extra is provided, wait for the metadata to be available, since it's
// still required for generating the lock file.
if !self.editables.contains(name) {
// Determine the distribution to lookup.
let dist = match url {
Some(url) => PubGrubDistribution::from_url(name, url),
None => PubGrubDistribution::from_registry(name, version),
};
let version_id = dist.version_id();
// Wait for the metadata to be available.
self.index
.distributions()
.wait_blocking(&version_id)
.ok_or(ResolveError::Unregistered)?;
}
let dist = match url {
Some(url) => PubGrubDistribution::from_url(name, url),
None => PubGrubDistribution::from_registry(name, version),
};
let version_id = dist.version_id();
// Wait for the metadata to be available.
self.index
.distributions()
.wait_blocking(&version_id)
.ok_or(ResolveError::Unregistered)?;
return Ok(Dependencies::Available(Vec::default()));
}
// Determine if the distribution is editable.
if let Some(editable) = self.editables.get(name) {
let requirements: Vec<_> = editable
.metadata
.requires_dist
.iter()
.cloned()
.map(Requirement::from)
.collect();
let dependencies = PubGrubDependencies::from_requirements(
&requirements,
&self.constraints,
&self.overrides,
Some(name),
extra.as_ref(),
&self.urls,
&self.locals,
self.markers.as_ref(),
)?;
for (dep_package, dep_version) in dependencies.iter() {
debug!("Adding transitive dependency for {package}=={version}: {dep_package}{dep_version}");
// Update the package priorities.
priorities.insert(dep_package, dep_version);
// Emit a request to fetch the metadata for this package.
self.visit_package(dep_package, request_sink)?;
}
return Ok(Dependencies::Available(dependencies.into()));
}
// Determine the distribution to lookup.
let dist = match url {
Some(url) => PubGrubDistribution::from_url(name, url),
@ -1232,6 +1115,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
ResolveError::FetchAndBuild(Box::new(source_dist), err)
}
})?;
Ok(Some(Response::Dist { dist, metadata }))
}
@ -1320,6 +1204,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
ResolveError::FetchAndBuild(Box::new(source_dist), err)
}
})?;
Response::Dist { dist, metadata }
}
ResolvedDist::Installed(dist) => {

View file

@ -22,36 +22,6 @@ impl Urls {
) -> Result<Self, ResolveError> {
let mut urls: FxHashMap<PackageName, VerbatimParsedUrl> = FxHashMap::default();
// Add the editables themselves to the list of required URLs.
for editable in &manifest.editables {
let editable_url = VerbatimParsedUrl {
parsed_url: ParsedUrl::Path(ParsedPathUrl {
url: editable.built.url.to_url(),
path: editable.built.path.clone(),
editable: true,
}),
verbatim: editable.built.url.clone(),
};
if let Some(previous) =
urls.insert(editable.metadata.name.clone(), editable_url.clone())
{
if !is_equal(&previous.verbatim, &editable_url.verbatim) {
if is_same_reference(&previous.verbatim, &editable_url.verbatim) {
debug!(
"Allowing {} as a variant of {}",
editable_url.verbatim, previous.verbatim
);
} else {
return Err(ResolveError::ConflictingUrlsDirect(
editable.metadata.name.clone(),
previous.verbatim.verbatim().to_string(),
editable_url.verbatim.verbatim().to_string(),
));
}
}
}
}
// Add all direct requirements and constraints. If there are any conflicts, return an error.
for requirement in manifest.requirements(markers, dependencies) {
match &requirement.source {

View file

@ -300,7 +300,6 @@ async fn black_mypy_extensions() -> Result<()> {
Overrides::default(),
vec![],
None,
vec![],
Exclusions::default(),
vec![],
);
@ -341,7 +340,6 @@ async fn black_mypy_extensions_extra() -> Result<()> {
Overrides::default(),
vec![],
None,
vec![],
Exclusions::default(),
vec![],
);
@ -382,7 +380,6 @@ async fn black_flake8() -> Result<()> {
Overrides::default(),
vec![],
None,
vec![],
Exclusions::default(),
vec![],
);
@ -480,7 +477,6 @@ async fn black_respect_preference() -> Result<()> {
Version::from_str("23.9.0")?,
)],
None,
vec![],
Exclusions::default(),
vec![],
);
@ -521,7 +517,6 @@ async fn black_ignore_preference() -> Result<()> {
Version::from_str("23.9.2")?,
)],
None,
vec![],
Exclusions::default(),
vec![],
);

View file

@ -19,7 +19,6 @@ install-wheel-rs = { workspace = true, features = ["clap"], default-features = f
pep508_rs = { workspace = true }
platform-tags = { workspace = true }
pypi-types = { workspace = true }
requirements-txt = { workspace = true, features = ["http"] }
uv-auth = { workspace = true }
uv-cache = { workspace = true, features = ["clap"] }
uv-client = { workspace = true }
@ -45,7 +44,6 @@ clap = { workspace = true, features = ["derive", "string", "wrap_help"] }
clap_complete_command = { workspace = true }
flate2 = { workspace = true, default-features = false }
fs-err = { workspace = true, features = ["tokio"] }
indexmap = { workspace = true }
indicatif = { workspace = true }
itertools = { workspace = true }
miette = { workspace = true, features = ["fancy"] }

View file

@ -7,21 +7,15 @@ use std::path::Path;
use std::str::FromStr;
use anstream::{eprint, AutoStream, StripStream};
use anyhow::{anyhow, Context, Result};
use anyhow::{anyhow, Result};
use fs_err as fs;
use indexmap::IndexMap;
use itertools::Itertools;
use owo_colors::OwoColorize;
use tempfile::tempdir_in;
use tracing::debug;
use distribution_types::{
IndexLocations, LocalEditable, LocalEditables, SourceAnnotation, SourceAnnotations, Verbatim,
};
use distribution_types::{Requirement, Requirements};
use distribution_types::{IndexLocations, SourceAnnotation, SourceAnnotations, Verbatim};
use install_wheel_rs::linker::LinkMode;
use platform_tags::Tags;
use requirements_txt::EditableRequirement;
use uv_auth::store_credentials_from_url;
use uv_cache::Cache;
use uv_client::{BaseClientBuilder, Connectivity, FlatIndexClient, RegistryClientBuilder};
@ -33,7 +27,6 @@ use uv_configuration::{KeyringProviderType, TargetTriple};
use uv_dispatch::BuildDispatch;
use uv_distribution::DistributionDatabase;
use uv_fs::Simplified;
use uv_installer::Downloader;
use uv_interpreter::{
find_best_interpreter, find_interpreter, InterpreterRequest, PythonEnvironment, SystemPython,
VersionRequest,
@ -45,15 +38,15 @@ use uv_requirements::{
RequirementsSource, RequirementsSpecification, SourceTreeResolver,
};
use uv_resolver::{
AnnotationStyle, BuiltEditableMetadata, DependencyMode, DisplayResolutionGraph, ExcludeNewer,
Exclusions, FlatIndex, InMemoryIndex, Manifest, OptionsBuilder, PreReleaseMode,
PythonRequirement, ResolutionMode, Resolver,
AnnotationStyle, DependencyMode, DisplayResolutionGraph, ExcludeNewer, Exclusions, FlatIndex,
InMemoryIndex, Manifest, OptionsBuilder, PreReleaseMode, PythonRequirement, ResolutionMode,
Resolver,
};
use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy, InFlight};
use uv_warnings::warn_user;
use crate::commands::pip::operations;
use crate::commands::reporters::{DownloadReporter, ResolverReporter};
use crate::commands::reporters::ResolverReporter;
use crate::commands::{elapsed, ExitStatus};
use crate::printer::Printer;
@ -123,7 +116,6 @@ pub(crate) async fn pip_compile(
requirements,
constraints,
overrides,
editables,
source_trees,
extras: used_extras,
index_url,
@ -420,98 +412,10 @@ pub(crate) async fn pip_compile(
}
}
for editable in &editables {
if let Some(origin) = &editable.origin {
sources.add_editable(
editable.url(),
SourceAnnotation::Requirement(origin.clone()),
);
}
}
// Collect constraints and overrides.
let constraints = Constraints::from_requirements(constraints);
let overrides = Overrides::from_requirements(overrides);
// Build the editables and add their requirements
let editables = if editables.is_empty() {
Vec::new()
} else {
let start = std::time::Instant::now();
let editables = LocalEditables::from_editables(editables.into_iter().map(|editable| {
let EditableRequirement {
url,
extras,
path,
marker: _,
origin: _,
} = editable;
LocalEditable { url, path, extras }
}));
let downloader = Downloader::new(
&cache,
&tags,
&hasher,
DistributionDatabase::new(&client, &build_dispatch, concurrency.downloads),
)
.with_reporter(DownloadReporter::from(printer).with_length(editables.len() as u64));
// Build all editables.
let editable_wheel_dir = tempdir_in(cache.root())?;
let editables: Vec<BuiltEditableMetadata> = downloader
.build_editables(editables, editable_wheel_dir.path())
.await
.context("Failed to build editables")?
.into_iter()
.map(|built_editable| {
let requirements = Requirements {
dependencies: built_editable
.metadata
.requires_dist
.iter()
.cloned()
.map(Requirement::from)
.collect(),
optional_dependencies: IndexMap::default(),
};
BuiltEditableMetadata {
built: built_editable.editable,
metadata: built_editable.metadata,
requirements,
}
})
.collect();
// Validate that the editables are compatible with the target Python version.
for editable in &editables {
if let Some(python_requires) = editable.metadata.requires_python.as_ref() {
if !python_requires.contains(python_requirement.target()) {
return Err(anyhow!(
"Editable `{}` requires Python {}, but resolution targets Python {}",
editable.metadata.name,
python_requires,
python_requirement.target()
));
}
}
}
let s = if editables.len() == 1 { "" } else { "s" };
writeln!(
printer.stderr(),
"{}",
format!(
"Built {} in {}",
format!("{} editable{}", editables.len(), s).bold(),
elapsed(start.elapsed())
)
.dimmed()
)?;
editables
};
// Determine any lookahead requirements.
let lookaheads = match dependency_mode {
DependencyMode::Transitive => {
@ -519,7 +423,6 @@ pub(crate) async fn pip_compile(
&requirements,
&constraints,
&overrides,
&editables,
&hasher,
&top_level_index,
DistributionDatabase::new(&client, &build_dispatch, concurrency.downloads),
@ -538,7 +441,6 @@ pub(crate) async fn pip_compile(
overrides,
preferences,
project,
editables,
// Do not consider any installed packages during resolution.
Exclusions::All,
lookaheads,

View file

@ -33,7 +33,6 @@ use uv_types::{BuildIsolation, HashStrategy, InFlight};
use crate::commands::pip::operations;
use crate::commands::pip::operations::Modifications;
use crate::commands::{elapsed, ExitStatus};
use crate::editables::ResolvedEditables;
use crate::printer::Printer;
/// Install packages into the current environment.
@ -89,7 +88,6 @@ pub(crate) async fn pip_install(
requirements,
constraints,
overrides,
editables,
source_trees,
index_url,
extra_index_urls,
@ -169,7 +167,7 @@ pub(crate) async fn pip_install(
&& overrides.is_empty()
&& uv_lock.is_none()
{
match site_packages.satisfies(&requirements, &editables, &constraints)? {
match site_packages.satisfies(&requirements, &constraints)? {
// If the requirements are already satisfied, we're done.
SatisfiesResult::Fresh {
recursive_requirements,
@ -183,13 +181,7 @@ pub(crate) async fn pip_install(
debug!("Requirement satisfied: {requirement}");
}
}
if !editables.is_empty() {
debug!(
"All editables satisfied: {}",
editables.iter().map(ToString::to_string).join(" | ")
);
}
let num_requirements = requirements.len() + editables.len();
let num_requirements = requirements.len();
let s = if num_requirements == 1 { "" } else { "s" };
writeln!(
printer.stderr(),
@ -326,25 +318,6 @@ pub(crate) async fn pip_install(
)
.with_options(OptionsBuilder::new().exclude_newer(exclude_newer).build());
// Build all editable distributions. The editables are shared between resolution and
// installation, and should live for the duration of the command.
let editables = ResolvedEditables::resolve(
editables
.into_iter()
.map(ResolvedEditables::from_requirement),
&site_packages,
&reinstall,
&hasher,
venv.interpreter(),
&tags,
&cache,
&client,
&resolve_dispatch,
concurrency,
printer,
)
.await?;
// Resolve the requirements.
let resolution = if let Some(ref root) = uv_lock {
let root = PackageName::new(root.to_string())?;
@ -367,7 +340,6 @@ pub(crate) async fn pip_install(
source_trees,
project,
extras,
&editables,
site_packages.clone(),
&hasher,
&reinstall,
@ -426,7 +398,6 @@ pub(crate) async fn pip_install(
// Sync the environment.
operations::install(
&resolution,
&editables,
site_packages,
Modifications::Sufficient,
&reinstall,

View file

@ -26,7 +26,7 @@ use uv_configuration::{
use uv_dispatch::BuildDispatch;
use uv_distribution::DistributionDatabase;
use uv_fs::Simplified;
use uv_installer::{Downloader, Plan, Planner, ResolvedEditable, SitePackages};
use uv_installer::{Downloader, Plan, Planner, SitePackages};
use uv_interpreter::{Interpreter, PythonEnvironment};
use uv_normalize::PackageName;
use uv_requirements::{
@ -43,7 +43,6 @@ use uv_warnings::warn_user;
use crate::commands::reporters::{DownloadReporter, InstallReporter, ResolverReporter};
use crate::commands::DryRunEvent;
use crate::commands::{compile_bytecode, elapsed, ChangeEvent, ChangeEventKind};
use crate::editables::ResolvedEditables;
use crate::printer::Printer;
/// Consolidate the requirements for an installation.
@ -110,7 +109,6 @@ pub(crate) async fn resolve<InstalledPackages: InstalledPackagesProvider>(
source_trees: Vec<PathBuf>,
project: Option<PackageName>,
extras: &ExtrasSpecification,
editables: &ResolvedEditables,
installed_packages: InstalledPackages,
hasher: &HashStrategy,
reinstall: &Reinstall,
@ -176,9 +174,6 @@ pub(crate) async fn resolve<InstalledPackages: InstalledPackagesProvider>(
let overrides = Overrides::from_requirements(overrides);
let python_requirement = PythonRequirement::from_marker_environment(interpreter, markers);
// Map the editables to their metadata.
let editables = editables.as_metadata();
// Determine any lookahead requirements.
let lookaheads = match options.dependency_mode {
DependencyMode::Transitive => {
@ -186,7 +181,6 @@ pub(crate) async fn resolve<InstalledPackages: InstalledPackagesProvider>(
&requirements,
&constraints,
&overrides,
&editables,
hasher,
index,
DistributionDatabase::new(client, build_dispatch, concurrency.downloads),
@ -215,7 +209,6 @@ pub(crate) async fn resolve<InstalledPackages: InstalledPackagesProvider>(
overrides,
preferences,
project,
editables,
exclusions,
lookaheads,
);
@ -282,7 +275,6 @@ pub(crate) enum Modifications {
#[allow(clippy::too_many_arguments)]
pub(crate) async fn install(
resolution: &Resolution,
editables: &[ResolvedEditable],
site_packages: SitePackages,
modifications: Modifications,
reinstall: &Reinstall,
@ -303,26 +295,12 @@ pub(crate) async fn install(
) -> Result<(), Error> {
let start = std::time::Instant::now();
// Extract the requirements from the resolution, filtering out any editables that were already
// required. If a package is already installed as editable, it may appear in the resolution
// despite not being explicitly requested.
let requirements = resolution
.requirements()
.filter(|requirement| {
if requirement.source.is_editable() {
!editables
.iter()
.any(|editable| requirement.name == *editable.name())
} else {
true
}
})
.collect::<Vec<_>>();
// Extract the requirements from the resolution.
let requirements = resolution.requirements().collect::<Vec<_>>();
// Partition into those that should be linked from the cache (`local`), those that need to be
// downloaded (`remote`), and those that should be removed (`extraneous`).
let plan = Planner::with_requirements(&requirements)
.with_editable_requirements(editables)
let plan = Planner::new(&requirements)
.build(
site_packages,
reinstall,

View file

@ -31,7 +31,6 @@ use uv_types::{BuildIsolation, HashStrategy, InFlight};
use crate::commands::pip::operations;
use crate::commands::pip::operations::Modifications;
use crate::commands::ExitStatus;
use crate::editables::ResolvedEditables;
use crate::printer::Printer;
/// Install a set of locked requirements into the current Python environment.
@ -86,7 +85,6 @@ pub(crate) async fn pip_sync(
requirements,
constraints,
overrides,
editables,
source_trees,
index_url,
extra_index_urls,
@ -107,7 +105,7 @@ pub(crate) async fn pip_sync(
.await?;
// Validate that the requirements are non-empty.
let num_requirements = requirements.len() + source_trees.len() + editables.len();
let num_requirements = requirements.len() + source_trees.len();
if num_requirements == 0 {
writeln!(printer.stderr(), "No requirements found")?;
return Ok(ExitStatus::Success);
@ -277,25 +275,6 @@ pub(crate) async fn pip_sync(
// Determine the set of installed packages.
let site_packages = SitePackages::from_executable(&venv)?;
// Build all editable distributions. The editables are shared between resolution and
// installation, and should live for the duration of the command.
let editables = ResolvedEditables::resolve(
editables
.into_iter()
.map(ResolvedEditables::from_requirement),
&site_packages,
reinstall,
&hasher,
venv.interpreter(),
&tags,
&cache,
&client,
&resolve_dispatch,
concurrency,
printer,
)
.await?;
let options = OptionsBuilder::new()
.resolution_mode(resolution_mode)
.prerelease_mode(prerelease_mode)
@ -311,7 +290,6 @@ pub(crate) async fn pip_sync(
source_trees,
project,
&extras,
&editables,
site_packages.clone(),
&hasher,
reinstall,
@ -369,7 +347,6 @@ pub(crate) async fn pip_sync(
// Sync the environment.
operations::install(
&resolution,
&editables,
site_packages,
Modifications::Exact,
reinstall,

View file

@ -16,7 +16,6 @@ use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy, InFlight};
use uv_warnings::warn_user;
use crate::commands::{pip, project, ExitStatus};
use crate::editables::ResolvedEditables;
use crate::printer::Printer;
/// Resolve the project requirements into a lockfile.
@ -104,26 +103,6 @@ pub(crate) async fn lock(
concurrency,
);
// Build all editable distributions. The editables are shared between resolution and
// installation, and should live for the duration of the command.
let editables = ResolvedEditables::resolve(
spec.editables
.iter()
.cloned()
.map(ResolvedEditables::from_requirement),
&EmptyInstalledPackages,
&reinstall,
&hasher,
&interpreter,
tags,
cache,
&client,
&build_dispatch,
concurrency,
printer,
)
.await?;
// Resolve the requirements.
let resolution = pip::operations::resolve(
spec.requirements,
@ -132,7 +111,6 @@ pub(crate) async fn lock(
spec.source_trees,
spec.project,
&extras,
&editables,
EmptyInstalledPackages,
&hasher,
&reinstall,

View file

@ -24,7 +24,6 @@ use uv_requirements::{
use uv_resolver::{FlatIndex, InMemoryIndex, Options};
use uv_types::{BuildIsolation, HashStrategy, InFlight};
use crate::editables::ResolvedEditables;
use crate::printer::Printer;
pub(crate) mod lock;
@ -118,7 +117,7 @@ pub(crate) async fn update_environment(
// Check if the current environment satisfies the requirements
let site_packages = SitePackages::from_executable(&venv)?;
if spec.source_trees.is_empty() {
match site_packages.satisfies(&spec.requirements, &spec.editables, &spec.constraints)? {
match site_packages.satisfies(&spec.requirements, &spec.constraints)? {
// If the requirements are already satisfied, we're done.
SatisfiesResult::Fresh {
recursive_requirements,
@ -131,12 +130,6 @@ pub(crate) async fn update_environment(
.sorted()
.join(" | ")
);
if !spec.editables.is_empty() {
debug!(
"All editables satisfied: {}",
spec.editables.iter().map(ToString::to_string).join(", ")
);
}
return Ok(venv);
}
SatisfiesResult::Unsatisfied(requirement) => {
@ -196,26 +189,6 @@ pub(crate) async fn update_environment(
concurrency,
);
// Build all editable distributions. The editables are shared between resolution and
// installation, and should live for the duration of the command.
let editables = ResolvedEditables::resolve(
spec.editables
.iter()
.cloned()
.map(ResolvedEditables::from_requirement),
&site_packages,
&reinstall,
&hasher,
&interpreter,
tags,
cache,
&client,
&resolve_dispatch,
concurrency,
printer,
)
.await?;
// Resolve the requirements.
let resolution = match pip::operations::resolve(
spec.requirements,
@ -224,7 +197,6 @@ pub(crate) async fn update_environment(
spec.source_trees,
spec.project,
&extras,
&editables,
site_packages.clone(),
&hasher,
&reinstall,
@ -275,7 +247,6 @@ pub(crate) async fn update_environment(
// Sync the environment.
pip::operations::install(
&resolution,
&editables,
site_packages,
pip::operations::Modifications::Sufficient,
&reinstall,

View file

@ -16,7 +16,6 @@ use uv_warnings::warn_user;
use crate::commands::pip::operations::Modifications;
use crate::commands::{pip, project, ExitStatus};
use crate::editables::ResolvedEditables;
use crate::printer::Printer;
/// Sync the project environment.
@ -90,28 +89,9 @@ pub(crate) async fn sync(
let site_packages = SitePackages::from_executable(&venv)?;
// Build any editables.
let editables = ResolvedEditables::resolve(
resolution.editables(),
&site_packages,
&reinstall,
&hasher,
venv.interpreter(),
tags,
cache,
&client,
&build_dispatch,
concurrency,
printer,
)
.await?;
let site_packages = SitePackages::from_executable(&venv)?;
// Sync the environment.
pip::operations::install(
&resolution,
&editables,
site_packages,
Modifications::Sufficient,
&reinstall,

View file

@ -7,8 +7,7 @@ use rustc_hash::FxHashMap;
use url::Url;
use distribution_types::{
BuildableSource, CachedDist, DistributionMetadata, LocalEditable, Name, SourceDist,
VersionOrUrlRef,
BuildableSource, CachedDist, DistributionMetadata, Name, SourceDist, VersionOrUrlRef,
};
use uv_normalize::PackageName;
@ -43,7 +42,7 @@ impl BarState {
}
impl ProgressReporter {
fn on_any_build_start(&self, color_string: &str) -> usize {
fn on_build_start(&self, source: &BuildableSource) -> usize {
let mut state = self.state.lock().unwrap();
let id = state.id();
@ -53,21 +52,29 @@ impl ProgressReporter {
);
progress.set_style(ProgressStyle::with_template("{wide_msg}").unwrap());
progress.set_message(format!("{} {}", "Building".bold().cyan(), color_string));
progress.set_message(format!(
"{} {}",
"Building".bold().cyan(),
source.to_color_string()
));
state.headers += 1;
state.bars.insert(id, progress);
id
}
fn on_any_build_complete(&self, color_string: &str, id: usize) {
fn on_build_complete(&self, source: &BuildableSource, id: usize) {
let progress = {
let mut state = self.state.lock().unwrap();
state.headers -= 1;
state.bars.remove(&id).unwrap()
};
progress.finish_with_message(format!(" {} {}", "Built".bold().green(), color_string));
progress.finish_with_message(format!(
" {} {}",
"Built".bold().green(),
source.to_color_string()
));
}
fn on_download_start(&self, name: &PackageName, size: Option<u64>) -> usize {
@ -198,21 +205,11 @@ impl uv_installer::DownloadReporter for DownloadReporter {
}
fn on_build_start(&self, source: &BuildableSource) -> usize {
self.reporter.on_any_build_start(&source.to_color_string())
self.reporter.on_build_start(source)
}
fn on_build_complete(&self, source: &BuildableSource, id: usize) {
self.reporter
.on_any_build_complete(&source.to_color_string(), id);
}
fn on_editable_build_start(&self, dist: &LocalEditable) -> usize {
self.reporter.on_any_build_start(&dist.to_color_string())
}
fn on_editable_build_complete(&self, dist: &LocalEditable, id: usize) {
self.reporter
.on_any_build_complete(&dist.to_color_string(), id);
self.reporter.on_build_complete(source, id);
}
fn on_download_start(&self, name: &PackageName, size: Option<u64>) -> usize {
@ -290,12 +287,11 @@ impl uv_resolver::ResolverReporter for ResolverReporter {
}
fn on_build_start(&self, source: &BuildableSource) -> usize {
self.reporter.on_any_build_start(&source.to_color_string())
self.reporter.on_build_start(source)
}
fn on_build_complete(&self, source: &BuildableSource, id: usize) {
self.reporter
.on_any_build_complete(&source.to_color_string(), id);
self.reporter.on_build_complete(source, id);
}
fn on_checkout_start(&self, url: &Url, rev: &str) -> usize {
@ -321,12 +317,11 @@ impl uv_resolver::ResolverReporter for ResolverReporter {
impl uv_distribution::Reporter for ResolverReporter {
fn on_build_start(&self, source: &BuildableSource) -> usize {
self.reporter.on_any_build_start(&source.to_color_string())
self.reporter.on_build_start(source)
}
fn on_build_complete(&self, source: &BuildableSource, id: usize) {
self.reporter
.on_any_build_complete(&source.to_color_string(), id);
self.reporter.on_build_complete(source, id);
}
fn on_download_start(&self, name: &PackageName, size: Option<u64>) -> usize {
@ -406,9 +401,3 @@ impl ColorDisplay for BuildableSource<'_> {
}
}
}
impl ColorDisplay for LocalEditable {
fn to_color_string(&self) -> String {
format!("{}", self.to_string().dimmed())
}
}

View file

@ -1,217 +0,0 @@
use std::fmt::Write;
use std::ops::Deref;
use anyhow::{anyhow, Context, Result};
use indexmap::IndexMap;
use owo_colors::OwoColorize;
use distribution_types::{
InstalledDist, LocalEditable, LocalEditables, Name, Requirement, Requirements,
};
use platform_tags::Tags;
use requirements_txt::EditableRequirement;
use uv_cache::{ArchiveTarget, ArchiveTimestamp, Cache};
use uv_client::RegistryClient;
use uv_configuration::{Concurrency, Reinstall};
use uv_dispatch::BuildDispatch;
use uv_distribution::DistributionDatabase;
use uv_installer::{is_dynamic, Downloader, InstalledEditable, ResolvedEditable};
use uv_interpreter::Interpreter;
use uv_resolver::BuiltEditableMetadata;
use uv_types::{HashStrategy, InstalledPackagesProvider};
use crate::commands::elapsed;
use crate::commands::reporters::DownloadReporter;
use crate::printer::Printer;
#[derive(Debug, Default)]
pub(crate) struct ResolvedEditables {
/// The set of resolved editables, including both those that were already installed and those
/// that were built.
pub(crate) editables: Vec<ResolvedEditable>,
/// The temporary directory in which the built editables were stored.
#[allow(dead_code)]
temp_dir: Option<tempfile::TempDir>,
}
impl Deref for ResolvedEditables {
type Target = [ResolvedEditable];
fn deref(&self) -> &Self::Target {
&self.editables
}
}
impl ResolvedEditables {
/// Resolve the set of editables that need to be installed.
#[allow(clippy::too_many_arguments)]
pub(crate) async fn resolve(
editables: impl IntoIterator<Item = LocalEditable>,
installed_packages: &impl InstalledPackagesProvider,
reinstall: &Reinstall,
hasher: &HashStrategy,
interpreter: &Interpreter,
tags: &Tags,
cache: &Cache,
client: &RegistryClient,
build_dispatch: &BuildDispatch<'_>,
concurrency: Concurrency,
printer: Printer,
) -> Result<Self> {
// Partition the editables into those that are already installed, and those that must be built.
let mut installed = Vec::new();
let mut builds = Vec::new();
for editable in editables {
match reinstall {
Reinstall::None => {
if let [dist] = installed_packages.get_editables(editable.raw()).as_slice() {
if let Some(editable) = up_to_date(&editable, dist)? {
installed.push(editable);
} else {
builds.push(editable);
}
} else {
builds.push(editable);
}
}
Reinstall::All => {
builds.push(editable);
}
Reinstall::Packages(packages) => {
if let [dist] = installed_packages.get_editables(editable.raw()).as_slice() {
if packages.contains(dist.name()) {
builds.push(editable);
} else if let Some(editable) = up_to_date(&editable, dist)? {
installed.push(editable);
} else {
builds.push(editable);
}
} else {
builds.push(editable);
}
}
}
}
// Build any editables.
let (built_editables, temp_dir) = if builds.is_empty() {
(Vec::new(), None)
} else {
let start = std::time::Instant::now();
let downloader = Downloader::new(
cache,
tags,
hasher,
DistributionDatabase::new(client, build_dispatch, concurrency.downloads),
)
.with_reporter(DownloadReporter::from(printer).with_length(builds.len() as u64));
let editables = LocalEditables::from_editables(builds.into_iter());
let temp_dir = tempfile::tempdir_in(cache.root())?;
let editables: Vec<_> = downloader
.build_editables(editables, temp_dir.path())
.await
.context("Failed to build editables")?
.into_iter()
.collect();
// Validate that the editables are compatible with the target Python version.
for editable in &editables {
if let Some(python_requires) = editable.metadata.requires_python.as_ref() {
if !python_requires.contains(interpreter.python_version()) {
return Err(anyhow!(
"Editable `{}` requires Python {}, but {} is installed",
editable.metadata.name,
python_requires,
interpreter.python_version()
));
}
}
}
let s = if editables.len() == 1 { "" } else { "s" };
writeln!(
printer.stderr(),
"{}",
format!(
"Built {} in {}",
format!("{} editable{}", editables.len(), s).bold(),
elapsed(start.elapsed())
)
.dimmed()
)?;
(editables, Some(temp_dir))
};
let editables = installed
.into_iter()
.map(ResolvedEditable::Installed)
.chain(built_editables.into_iter().map(ResolvedEditable::Built))
.collect::<Vec<_>>();
Ok(Self {
editables,
temp_dir,
})
}
pub(crate) fn as_metadata(&self) -> Vec<BuiltEditableMetadata> {
self.iter()
.map(|editable| {
let dependencies: Vec<_> = editable
.metadata()
.requires_dist
.iter()
.cloned()
.map(Requirement::from)
.collect();
BuiltEditableMetadata {
built: editable.local().clone(),
metadata: editable.metadata().clone(),
requirements: Requirements {
dependencies,
optional_dependencies: IndexMap::default(),
},
}
})
.collect()
}
/// Convert an [`EditableRequirement`] into a [`LocalEditable`].
pub(crate) fn from_requirement(editable: EditableRequirement) -> LocalEditable {
LocalEditable {
url: editable.url,
path: editable.path,
extras: editable.extras,
}
}
}
/// Returns the [`InstalledEditable`] if the installed distribution is up-to-date for the given
/// requirement.
fn up_to_date(editable: &LocalEditable, dist: &InstalledDist) -> Result<Option<InstalledEditable>> {
// If the editable isn't up-to-date, don't reuse it.
if !ArchiveTimestamp::up_to_date_with(&editable.path, ArchiveTarget::Install(dist))? {
return Ok(None);
};
// If the editable is dynamic, don't reuse it.
if is_dynamic(&editable.path) {
return Ok(None);
};
// If we can't read the metadata from the installed distribution, don't reuse it.
let Ok(metadata) = dist.metadata() else {
return Ok(None);
};
Ok(Some(InstalledEditable {
editable: editable.clone(),
wheel: (*dist).clone(),
metadata,
}))
}

View file

@ -44,7 +44,6 @@ static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc;
mod cli;
mod commands;
mod compat;
mod editables;
mod logging;
mod printer;
mod settings;

View file

@ -3377,7 +3377,6 @@ fn compile_editable() -> Result<()> {
# via aiohttp
----- stderr -----
Built 2 editables in [TIME]
Resolved 13 packages in [TIME]
"###);
@ -3428,7 +3427,6 @@ fn deduplicate_editable() -> Result<()> {
# via aiohttp
----- stderr -----
Built 1 editable in [TIME]
Resolved 9 packages in [TIME]
"###);
@ -3507,7 +3505,6 @@ fn compile_editable_url_requirement() -> Result<()> {
# via hatchling-editable
----- stderr -----
Built 1 editable in [TIME]
Resolved 2 packages in [TIME]
"###);
@ -4009,7 +4006,6 @@ fn generate_hashes_editable() -> Result<()> {
# via anyio
----- stderr -----
Built 1 editable in [TIME]
Resolved 4 packages in [TIME]
"###);
@ -4219,7 +4215,6 @@ coverage = ["example[test]", "extras>=0.0.1,<=0.0.2"]
# via extras
----- stderr -----
Built 1 editable in [TIME]
Resolved 3 packages in [TIME]
"###
);
@ -4436,8 +4431,7 @@ fn missing_editable_requirement() -> Result<()> {
----- stdout -----
----- stderr -----
error: Failed to build editables
Caused by: Source distribution not found at: [TEMP_DIR]/foo/anyio-3.7.0.tar.gz
error: Distribution not found at: file://[TEMP_DIR]/foo/anyio-3.7.0.tar.gz
"###);
Ok(())
@ -5610,7 +5604,6 @@ dependencies = [
# via -r requirements.in
----- stderr -----
Built 2 editables in [TIME]
Resolved 2 packages in [TIME]
"###
);
@ -5637,7 +5630,6 @@ fn editable_invalid_extra() -> Result<()> {
# via -r [TEMP_DIR]/requirements.in
----- stderr -----
Built 1 editable in [TIME]
Resolved 1 package in [TIME]
warning: The package `black @ file://[WORKSPACE]/scripts/packages/black_editable` does not have an extra named `empty`.
"###);
@ -5882,9 +5874,7 @@ fn conflicting_url_markers() -> Result<()> {
Ok(())
}
/// Override a regular package with an editable.
///
/// At present, this incorrectly resolves to the regular package.
/// Override a regular package with an editable. This should resolve to the editable package.
#[test]
fn editable_override() -> Result<()> {
let context = TestContext::new("3.12");
@ -5895,41 +5885,32 @@ fn editable_override() -> Result<()> {
// Add an editable override.
let overrides_txt = context.temp_dir.child("overrides.txt");
overrides_txt.write_str("-e file://../../scripts/packages/black_editable")?;
overrides_txt.write_str("-e ../../scripts/packages/black_editable")?;
uv_snapshot!(context.compile()
.arg("requirements.in")
.arg("--override")
.arg("overrides.txt"), @r###"
uv_snapshot!(context.filters(), context.compile()
.arg(requirements_in.path())
.arg("--override")
.arg(overrides_txt.path())
.current_dir(current_dir()?), @r###"
success: true
exit_code: 0
----- stdout -----
# This file was autogenerated by uv via the following command:
# uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in --override overrides.txt
black==24.3.0
# via -r requirements.in
click==8.1.7
# via black
mypy-extensions==1.0.0
# via black
packaging==24.0
# via black
pathspec==0.12.1
# via black
platformdirs==4.2.0
# via black
# uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z [TEMP_DIR]/requirements.in --override [TEMP_DIR]/overrides.txt
-e ../../scripts/packages/black_editable
# via
# --override [TEMP_DIR]/overrides.txt
# -r [TEMP_DIR]/requirements.in
----- stderr -----
Resolved 6 packages in [TIME]
Resolved 1 package in [TIME]
"###
);
Ok(())
}
/// Override an editable with a regular package.
///
/// At present, this incorrectly resolves to the editable.
/// Override an editable with a regular package. This should resolve to the regular package.
#[test]
fn override_editable() -> Result<()> {
let context = TestContext::new("3.12");
@ -5949,12 +5930,23 @@ fn override_editable() -> Result<()> {
----- stdout -----
# This file was autogenerated by uv via the following command:
# uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z [TEMP_DIR]/requirements.in --override [TEMP_DIR]/overrides.txt
-e ../../scripts/packages/black_editable
# via -r [TEMP_DIR]/requirements.in
black==23.10.1
# via
# --override [TEMP_DIR]/overrides.txt
# -r [TEMP_DIR]/requirements.in
click==8.1.7
# via black
mypy-extensions==1.0.0
# via black
packaging==24.0
# via black
pathspec==0.12.1
# via black
platformdirs==4.2.0
# via black
----- stderr -----
Built 1 editable in [TIME]
Resolved 1 package in [TIME]
Resolved 6 packages in [TIME]
"###);
Ok(())
@ -6306,7 +6298,6 @@ fn editable_direct_dependency() -> Result<()> {
# via setuptools-editable
----- stderr -----
Built 1 editable in [TIME]
Resolved 2 packages in [TIME]
"###);
@ -6498,11 +6489,13 @@ requires-python = "<=3.8"
uv_snapshot!(context.filters(), context.compile()
.arg("requirements.in"), @r###"
success: false
exit_code: 2
exit_code: 1
----- stdout -----
----- stderr -----
error: Editable `example` requires Python <=3.8, but resolution targets Python 3.12.[X]
× No solution found when resolving dependencies:
Because the current Python version (3.12.[X]) does not satisfy Python<=3.8 and example==0.0.0 depends on Python<=3.8, we can conclude that example==0.0.0 cannot be used.
And because only example==0.0.0 is available and you require example, we can conclude that the requirements are unsatisfiable.
"###
);
@ -6548,11 +6541,13 @@ requires-python = "<=3.8"
.arg("requirements.in")
.arg("--python-version=3.11"), @r###"
success: false
exit_code: 2
exit_code: 1
----- stdout -----
----- stderr -----
error: Editable `example` requires Python <=3.8, but resolution targets Python 3.11
× No solution found when resolving dependencies:
Because the requested Python version (3.11) does not satisfy Python<=3.8 and example==0.0.0 depends on Python<=3.8, we can conclude that example==0.0.0 cannot be used.
And because only example==0.0.0 is available and you require example, we can conclude that the requirements are unsatisfiable.
"###
);
@ -6600,7 +6595,6 @@ dev = [
# via anyio
----- stderr -----
Built 1 editable in [TIME]
Resolved 4 packages in [TIME]
"###
);
@ -6651,7 +6645,6 @@ dev = ["setuptools"]
# via example
----- stderr -----
Built 1 editable in [TIME]
Resolved 4 packages in [TIME]
"###
);
@ -6810,7 +6803,6 @@ fn compile_root_uri_editable() -> Result<()> {
# via root-editable
----- stderr -----
Built 1 editable in [TIME]
Resolved 2 packages in [TIME]
"###
);
@ -7943,7 +7935,7 @@ requires-python = ">3.8"
# via -r requirements.in
idna==3.6
# via anyio
lib @ file://[TEMP_DIR]/lib
lib @ file://[TEMP_DIR]/lib/
# via example
----- stderr -----
@ -8055,7 +8047,6 @@ requires-python = ">3.8"
# via flask
----- stderr -----
Built 1 editable in [TIME]
Resolved 7 packages in [TIME]
"###
);
@ -9260,7 +9251,6 @@ fn tool_uv_sources() -> Result<()> {
# via project (some_dir/pyproject.toml)
----- stderr -----
Built 1 editable in [TIME]
Resolved 8 packages in [TIME]
"###
);

View file

@ -848,9 +848,8 @@ fn install_editable() {
----- stdout -----
----- stderr -----
Built 1 editable in [TIME]
Resolved 4 packages in [TIME]
Downloaded 3 packages in [TIME]
Downloaded 4 packages in [TIME]
Installed 4 packages in [TIME]
+ anyio==4.3.0
+ idna==3.6
@ -928,8 +927,8 @@ fn install_editable_and_registry() {
----- stdout -----
----- stderr -----
Built 1 editable in [TIME]
Resolved 1 package in [TIME]
Downloaded 1 package in [TIME]
Uninstalled 1 package in [TIME]
Installed 1 package in [TIME]
- black==24.3.0
@ -993,8 +992,8 @@ fn install_editable_no_binary() {
----- stdout -----
----- stderr -----
Built 1 editable in [TIME]
Resolved 1 package in [TIME]
Downloaded 1 package in [TIME]
Installed 1 package in [TIME]
+ black==0.1.0 (from file://[WORKSPACE]/scripts/packages/black_editable)
"###
@ -1019,8 +1018,8 @@ fn install_editable_compatible_constraint() -> Result<()> {
----- stdout -----
----- stderr -----
Built 1 editable in [TIME]
Resolved 1 package in [TIME]
Downloaded 1 package in [TIME]
Installed 1 package in [TIME]
+ black==0.1.0 (from file://[WORKSPACE]/scripts/packages/black_editable)
"###
@ -1047,9 +1046,8 @@ fn install_editable_incompatible_constraint_version() -> Result<()> {
----- stdout -----
----- stderr -----
Built 1 editable in [TIME]
× No solution found when resolving dependencies:
Because you require black==0.1.0 and black>0.1.0, we can conclude that the requirements are unsatisfiable.
Because only black<=0.1.0 is available and you require black>0.1.0, we can conclude that the requirements are unsatisfiable.
"###
);
@ -1074,7 +1072,6 @@ fn install_editable_incompatible_constraint_url() -> Result<()> {
----- stdout -----
----- stderr -----
Built 1 editable in [TIME]
error: Requirements contain conflicting URLs for package `black`:
- [WORKSPACE]/scripts/packages/black_editable
- https://files.pythonhosted.org/packages/0f/89/294c9a6b6c75a08da55e9d05321d0707e9418735e3062b12ef0f54c33474/black-24.4.2-py3-none-any.whl
@ -1725,8 +1722,8 @@ fn only_binary_editable() {
----- stdout -----
----- stderr -----
Built 1 editable in [TIME]
Resolved 1 package in [TIME]
Downloaded 1 package in [TIME]
Installed 1 package in [TIME]
+ anyio==4.3.0+foo (from file://[WORKSPACE]/scripts/packages/anyio_local)
"###
@ -1739,26 +1736,26 @@ fn only_binary_dependent_editables() {
let context = TestContext::new("3.12");
let root_path = context
.workspace_root
.join("scripts/packages/dependent_editables");
.join("scripts/packages/dependent_locals");
// Install the editable package.
uv_snapshot!(context.filters(), context.install()
.arg("--only-binary")
.arg(":all:")
.arg("-e")
.arg(root_path.join("first_editable"))
.arg(root_path.join("first_local"))
.arg("-e")
.arg(root_path.join("second_editable")), @r###"
.arg(root_path.join("second_local")), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Built 2 editables in [TIME]
Resolved 2 packages in [TIME]
Downloaded 2 packages in [TIME]
Installed 2 packages in [TIME]
+ first-editable==0.0.1 (from file://[WORKSPACE]/scripts/packages/dependent_editables/first_editable)
+ second-editable==0.0.1 (from file://[WORKSPACE]/scripts/packages/dependent_editables/second_editable)
+ first-local==0.1.0 (from file://[WORKSPACE]/scripts/packages/dependent_locals/first_local)
+ second-local==0.1.0 (from file://[WORKSPACE]/scripts/packages/dependent_locals/second_local)
"###
);
}
@ -1779,9 +1776,8 @@ fn only_binary_editable_setup_py() {
----- stdout -----
----- stderr -----
Built 1 editable in [TIME]
Resolved 8 packages in [TIME]
Downloaded 7 packages in [TIME]
Downloaded 8 packages in [TIME]
Installed 8 packages in [TIME]
+ anyio==4.3.0
+ certifi==2024.2.2
@ -1949,8 +1945,8 @@ fn no_deps_editable() {
----- stdout -----
----- stderr -----
Built 1 editable in [TIME]
Resolved 1 package in [TIME]
Downloaded 1 package in [TIME]
Installed 1 package in [TIME]
+ black==0.1.0 (from file://[WORKSPACE]/scripts/packages/black_editable)
"###
@ -2408,9 +2404,8 @@ fn config_settings() {
----- stdout -----
----- stderr -----
Built 1 editable in [TIME]
Resolved 2 packages in [TIME]
Downloaded 1 package in [TIME]
Downloaded 2 packages in [TIME]
Installed 2 packages in [TIME]
+ iniconfig==2.0.0
+ setuptools-editable==0.1.0 (from file://[WORKSPACE]/scripts/packages/setuptools_editable)
@ -2437,9 +2432,8 @@ fn config_settings() {
----- stdout -----
----- stderr -----
Built 1 editable in [TIME]
Resolved 2 packages in [TIME]
Downloaded 1 package in [TIME]
Downloaded 2 packages in [TIME]
Installed 2 packages in [TIME]
+ iniconfig==2.0.0
+ setuptools-editable==0.1.0 (from file://[WORKSPACE]/scripts/packages/setuptools_editable)
@ -2575,9 +2569,8 @@ requires-python = ">=3.8"
----- stdout -----
----- stderr -----
Built 1 editable in [TIME]
Resolved 4 packages in [TIME]
Downloaded 3 packages in [TIME]
Downloaded 4 packages in [TIME]
Installed 4 packages in [TIME]
+ anyio==4.0.0
+ example==0.0.0 (from file://[TEMP_DIR]/editable)
@ -2620,9 +2613,8 @@ requires-python = ">=3.8"
----- stdout -----
----- stderr -----
Built 1 editable in [TIME]
Resolved 4 packages in [TIME]
Downloaded 1 package in [TIME]
Downloaded 2 packages in [TIME]
Uninstalled 2 packages in [TIME]
Installed 2 packages in [TIME]
- anyio==4.0.0
@ -2667,9 +2659,8 @@ dependencies = {file = ["requirements.txt"]}
----- stdout -----
----- stderr -----
Built 1 editable in [TIME]
Resolved 4 packages in [TIME]
Downloaded 3 packages in [TIME]
Downloaded 4 packages in [TIME]
Installed 4 packages in [TIME]
+ anyio==4.0.0
+ example==0.1.0 (from file://[TEMP_DIR]/editable)
@ -2687,8 +2678,8 @@ dependencies = {file = ["requirements.txt"]}
----- stdout -----
----- stderr -----
Built 1 editable in [TIME]
Resolved 4 packages in [TIME]
Downloaded 1 package in [TIME]
Uninstalled 1 package in [TIME]
Installed 1 package in [TIME]
- example==0.1.0 (from file://[TEMP_DIR]/editable)
@ -2708,9 +2699,8 @@ dependencies = {file = ["requirements.txt"]}
----- stdout -----
----- stderr -----
Built 1 editable in [TIME]
Resolved 4 packages in [TIME]
Downloaded 1 package in [TIME]
Downloaded 2 packages in [TIME]
Uninstalled 2 packages in [TIME]
Installed 2 packages in [TIME]
- anyio==4.0.0
@ -2837,9 +2827,8 @@ requires-python = ">=3.11,<3.13"
----- stdout -----
----- stderr -----
Built 1 editable in [TIME]
Resolved 4 packages in [TIME]
Downloaded 3 packages in [TIME]
Downloaded 4 packages in [TIME]
Installed 4 packages in [TIME]
+ anyio==4.0.0
+ example==0.1.0 (from file://[TEMP_DIR]/editable)
@ -2875,11 +2864,13 @@ requires-python = "<=3.8"
.arg("--editable")
.arg(editable_dir.path()), @r###"
success: false
exit_code: 2
exit_code: 1
----- stdout -----
----- stderr -----
error: Editable `example` requires Python <=3.8, but 3.12.[X] is installed
× No solution found when resolving dependencies:
Because the current Python version (3.12.[X]) does not satisfy Python<=3.8 and example==0.0.0 depends on Python<=3.8, we can conclude that example==0.0.0 cannot be used.
And because only example==0.0.0 is available and you require example, we can conclude that the requirements are unsatisfiable.
"###
);
@ -3889,11 +3880,12 @@ fn already_installed_dependent_editable() {
let context = TestContext::new("3.12");
let root_path = context
.workspace_root
.join("scripts/packages/dependent_editables");
.join("scripts/packages/dependent_locals");
// Install the first editable
uv_snapshot!(context.filters(), context.install()
.arg(root_path.join("first_editable")), @r###"
.arg("-e")
.arg(root_path.join("first_local")), @r###"
success: true
exit_code: 0
----- stdout -----
@ -3902,14 +3894,15 @@ fn already_installed_dependent_editable() {
Resolved 1 package in [TIME]
Downloaded 1 package in [TIME]
Installed 1 package in [TIME]
+ first-editable==0.0.1 (from file://[WORKSPACE]/scripts/packages/dependent_editables/first_editable)
+ first-local==0.1.0 (from file://[WORKSPACE]/scripts/packages/dependent_locals/first_local)
"###
);
// Install the second editable which depends on the first editable
// The already installed first editable package should satisfy the requirement
uv_snapshot!(context.filters(), context.install()
.arg(root_path.join("second_editable"))
.arg("-e")
.arg(root_path.join("second_local"))
// Disable the index to guard this test against dependency confusion attacks
.arg("--no-index")
.arg("--find-links")
@ -3922,14 +3915,15 @@ fn already_installed_dependent_editable() {
Resolved 2 packages in [TIME]
Downloaded 1 package in [TIME]
Installed 1 package in [TIME]
+ second-editable==0.0.1 (from file://[WORKSPACE]/scripts/packages/dependent_editables/second_editable)
+ second-local==0.1.0 (from file://[WORKSPACE]/scripts/packages/dependent_locals/second_local)
"###
);
// Request install of the first editable by full path again
// We should audit the installed package
uv_snapshot!(context.filters(), context.install()
.arg(root_path.join("first_editable")), @r###"
.arg("-e")
.arg(root_path.join("first_local")), @r###"
success: true
exit_code: 0
----- stdout -----
@ -3940,11 +3934,12 @@ fn already_installed_dependent_editable() {
);
// Request reinstallation of the first package during install of the second
// It's not available on an index and the user has not specified the path so we fail
// It's not available on an index and the user has not specified the path so we fail.
uv_snapshot!(context.filters(), context.install()
.arg(root_path.join("second_editable"))
.arg("-e")
.arg(root_path.join("second_local"))
.arg("--reinstall-package")
.arg("first-editable")
.arg("first-local")
// Disable the index to guard this test against dependency confusion attacks
.arg("--no-index")
.arg("--find-links")
@ -3955,27 +3950,29 @@ fn already_installed_dependent_editable() {
----- stderr -----
× No solution found when resolving dependencies:
Because first-editable was not found in the provided package locations and second-editable==0.0.1 depends on first-editable, we can conclude that second-editable==0.0.1 cannot be used.
And because only second-editable==0.0.1 is available and you require second-editable, we can conclude that the requirements are unsatisfiable.
Because first-local was not found in the provided package locations and second-local==0.1.0 depends on first-local, we can conclude that second-local==0.1.0 cannot be used.
And because only second-local==0.1.0 is available and you require second-local, we can conclude that the requirements are unsatisfiable.
"###
);
// Request reinstallation of the first package
// We include it in the install command with a full path so we should succeed
uv_snapshot!(context.filters(), context.install()
.arg(root_path.join("first_editable"))
.arg("-e")
.arg(root_path.join("first_local"))
.arg("--reinstall-package")
.arg("first-editable"), @r###"
.arg("first-local"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 1 package in [TIME]
Downloaded 1 package in [TIME]
Uninstalled 1 package in [TIME]
Installed 1 package in [TIME]
- first-editable==0.0.1 (from file://[WORKSPACE]/scripts/packages/dependent_editables/first_editable)
+ first-editable==0.0.1 (from file://[WORKSPACE]/scripts/packages/dependent_editables/first_editable)
- first-local==0.1.0 (from file://[WORKSPACE]/scripts/packages/dependent_locals/first_local)
+ first-local==0.1.0 (from file://[WORKSPACE]/scripts/packages/dependent_locals/first_local)
"###
);
}
@ -4070,6 +4067,7 @@ fn already_installed_local_path_dependent() {
----- stderr -----
Resolved 2 packages in [TIME]
Downloaded 1 package in [TIME]
Uninstalled 1 package in [TIME]
Installed 1 package in [TIME]
- first-local==0.1.0 (from file://[WORKSPACE]/scripts/packages/dependent_locals/first_local)
@ -4198,6 +4196,7 @@ fn already_installed_local_version_of_remote_package() {
----- stderr -----
Resolved 1 package in [TIME]
Downloaded 1 package in [TIME]
Uninstalled 1 package in [TIME]
Installed 1 package in [TIME]
- anyio==4.3.0+foo (from file://[WORKSPACE]/scripts/packages/anyio_local)
@ -4235,6 +4234,7 @@ fn already_installed_local_version_of_remote_package() {
----- stderr -----
Resolved 1 package in [TIME]
Downloaded 1 package in [TIME]
Uninstalled 1 package in [TIME]
Installed 1 package in [TIME]
- anyio==4.3.0
@ -4688,8 +4688,7 @@ fn require_hashes_editable() -> Result<()> {
----- stdout -----
----- stderr -----
Built 1 editable in [TIME]
error: In `--require-hashes` mode, all requirements must be pinned upfront with `==`, but found: `aiohttp`
error: In `--require-hashes` mode, all requirement must have a hash, but none were provided for: file://[WORKSPACE]/scripts/packages/black_editable[d]
"###
);
@ -4953,9 +4952,8 @@ fn tool_uv_sources() -> Result<()> {
----- stdout -----
----- stderr -----
Built 1 editable in [TIME]
Resolved 9 packages in [TIME]
Downloaded 8 packages in [TIME]
Downloaded 9 packages in [TIME]
Installed 9 packages in [TIME]
+ anyio==4.3.0
+ boltons==24.0.1.dev0 (from git+https://github.com/mahmoud/boltons@57fbaa9b673ed85b32458b31baeeae230520e4a0)

View file

@ -165,9 +165,8 @@ fn list_editable() {
----- stdout -----
----- stderr -----
Built 1 editable in [TIME]
Resolved 4 packages in [TIME]
Downloaded 3 packages in [TIME]
Downloaded 4 packages in [TIME]
Installed 4 packages in [TIME]
+ anyio==4.3.0
+ idna==3.6
@ -218,9 +217,8 @@ fn list_editable_only() {
----- stdout -----
----- stderr -----
Built 1 editable in [TIME]
Resolved 4 packages in [TIME]
Downloaded 3 packages in [TIME]
Downloaded 4 packages in [TIME]
Installed 4 packages in [TIME]
+ anyio==4.3.0
+ idna==3.6
@ -309,9 +307,8 @@ fn list_exclude() {
----- stdout -----
----- stderr -----
Built 1 editable in [TIME]
Resolved 4 packages in [TIME]
Downloaded 3 packages in [TIME]
Downloaded 4 packages in [TIME]
Installed 4 packages in [TIME]
+ anyio==4.3.0
+ idna==3.6
@ -413,9 +410,8 @@ fn list_format_json() {
----- stdout -----
----- stderr -----
Built 1 editable in [TIME]
Resolved 4 packages in [TIME]
Downloaded 3 packages in [TIME]
Downloaded 4 packages in [TIME]
Installed 4 packages in [TIME]
+ anyio==4.3.0
+ idna==3.6
@ -520,9 +516,8 @@ fn list_format_freeze() {
----- stdout -----
----- stderr -----
Built 1 editable in [TIME]
Resolved 4 packages in [TIME]
Downloaded 3 packages in [TIME]
Downloaded 4 packages in [TIME]
Installed 4 packages in [TIME]
+ anyio==4.3.0
+ idna==3.6

View file

@ -2198,9 +2198,8 @@ fn sync_editable() -> Result<()> {
----- stdout -----
----- stderr -----
Built 1 editable in [TIME]
Resolved 3 packages in [TIME]
Downloaded 2 packages in [TIME]
Downloaded 3 packages in [TIME]
Installed 3 packages in [TIME]
+ boltons==23.1.1
+ numpy==1.26.2
@ -2218,8 +2217,8 @@ fn sync_editable() -> Result<()> {
----- stdout -----
----- stderr -----
Built 1 editable in [TIME]
Resolved 3 packages in [TIME]
Downloaded 1 package in [TIME]
Uninstalled 1 package in [TIME]
Installed 1 package in [TIME]
- poetry-editable==0.1.0 (from file://[TEMP_DIR]/poetry_editable)
@ -2329,8 +2328,8 @@ fn sync_editable_and_registry() -> Result<()> {
----- stdout -----
----- stderr -----
Built 1 editable in [TIME]
Resolved 1 package in [TIME]
Downloaded 1 package in [TIME]
Uninstalled 1 package in [TIME]
Installed 1 package in [TIME]
- black==24.1.0
@ -2416,8 +2415,8 @@ fn sync_editable_and_local() -> Result<()> {
----- stdout -----
----- stderr -----
Built 1 editable in [TIME]
Resolved 1 package in [TIME]
Downloaded 1 package in [TIME]
Installed 1 package in [TIME]
+ black==0.1.0 (from file://[TEMP_DIR]/black_editable)
"###
@ -2460,8 +2459,8 @@ fn sync_editable_and_local() -> Result<()> {
----- stdout -----
----- stderr -----
Built 1 editable in [TIME]
Resolved 1 package in [TIME]
Downloaded 1 package in [TIME]
Uninstalled 1 package in [TIME]
Installed 1 package in [TIME]
- black==0.1.0 (from file://[TEMP_DIR]/black_editable)
@ -3099,8 +3098,8 @@ requires-python = ">=3.8"
----- stdout -----
----- stderr -----
Built 1 editable in [TIME]
Resolved 1 package in [TIME]
Downloaded 1 package in [TIME]
Installed 1 package in [TIME]
+ example==0.0.0 (from file://[TEMP_DIR]/editable)
"###
@ -3139,8 +3138,8 @@ requires-python = ">=3.8"
----- stdout -----
----- stderr -----
Built 1 editable in [TIME]
Resolved 1 package in [TIME]
Downloaded 1 package in [TIME]
Uninstalled 1 package in [TIME]
Installed 1 package in [TIME]
- example==0.0.0 (from file://[TEMP_DIR]/editable)
@ -3262,11 +3261,13 @@ requires-python = "<=3.5"
uv_snapshot!(context.filters(), sync_without_exclude_newer(&context)
.arg("requirements.in"), @r###"
success: false
exit_code: 2
exit_code: 1
----- stdout -----
----- stderr -----
error: Editable `example` requires Python <=3.5, but 3.12.[X] is installed
× No solution found when resolving dependencies:
Because the current Python version (3.12.[X]) does not satisfy Python<=3.5 and example==0.0.0 depends on Python<=3.5, we can conclude that example==0.0.0 cannot be used.
And because only example==0.0.0 is available and you require example, we can conclude that the requirements are unsatisfiable.
"###
);
@ -4208,7 +4209,7 @@ fn require_hashes_unnamed() -> Result<()> {
Ok(())
}
/// We allow `--require-hashes` for editables, as long as no dependencies are included.
/// We disallow `--require-hashes` for editables.
#[test]
fn require_hashes_editable() -> Result<()> {
let context = TestContext::new("3.12");
@ -4224,15 +4225,12 @@ fn require_hashes_editable() -> Result<()> {
uv_snapshot!(context.filters(), sync_without_exclude_newer(&context)
.arg(requirements_txt.path())
.arg("--require-hashes"), @r###"
success: true
exit_code: 0
success: false
exit_code: 2
----- stdout -----
----- stderr -----
Built 1 editable in [TIME]
Resolved 1 package in [TIME]
Installed 1 package in [TIME]
+ black==0.1.0 (from file://[WORKSPACE]/scripts/packages/black_editable)
error: In `--require-hashes` mode, all requirement must have a hash, but none were provided for: file://[WORKSPACE]/scripts/packages/black_editable[d]
"###
);

View file

@ -59,9 +59,8 @@ fn test_albatross_in_examples_bird_feeder() {
----- stdout -----
----- stderr -----
Built 1 editable in [TIME]
Resolved 4 packages in [TIME]
Downloaded 3 packages in [TIME]
Downloaded 4 packages in [TIME]
Installed 4 packages in [TIME]
+ anyio==4.3.0
+ bird-feeder==1.0.0 (from file://[WORKSPACE]/scripts/workspaces/albatross-in-example/examples/bird-feeder)
@ -95,9 +94,8 @@ fn test_albatross_in_examples() {
----- stdout -----
----- stderr -----
Built 1 editable in [TIME]
Resolved 2 packages in [TIME]
Downloaded 1 package in [TIME]
Downloaded 2 packages in [TIME]
Installed 2 packages in [TIME]
+ albatross==0.1.0 (from file://[WORKSPACE]/scripts/workspaces/albatross-in-example)
+ tqdm==4.66.2
@ -129,9 +127,8 @@ fn test_albatross_just_project() {
----- stdout -----
----- stderr -----
Built 1 editable in [TIME]
Resolved 2 packages in [TIME]
Downloaded 1 package in [TIME]
Downloaded 2 packages in [TIME]
Installed 2 packages in [TIME]
+ albatross==0.1.0 (from file://[WORKSPACE]/scripts/workspaces/albatross-just-project)
+ tqdm==4.66.2
@ -166,9 +163,8 @@ fn test_albatross_project_in_excluded() {
----- stdout -----
----- stderr -----
Built 1 editable in [TIME]
Resolved 4 packages in [TIME]
Downloaded 3 packages in [TIME]
Downloaded 4 packages in [TIME]
Installed 4 packages in [TIME]
+ anyio==4.3.0
+ bird-feeder==1.0.0 (from file://[WORKSPACE]/scripts/workspaces/albatross-project-in-excluded/excluded/bird-feeder)
@ -202,9 +198,8 @@ fn test_albatross_root_workspace() {
----- stdout -----
----- stderr -----
Built 3 editables in [TIME]
Resolved 7 packages in [TIME]
Downloaded 4 packages in [TIME]
Downloaded 7 packages in [TIME]
Installed 7 packages in [TIME]
+ albatross==0.1.0 (from file://[WORKSPACE]/scripts/workspaces/albatross-root-workspace)
+ anyio==4.3.0
@ -244,9 +239,8 @@ fn test_albatross_root_workspace_bird_feeder() {
----- stdout -----
----- stderr -----
Built 2 editables in [TIME]
Resolved 5 packages in [TIME]
Downloaded 3 packages in [TIME]
Downloaded 5 packages in [TIME]
Installed 5 packages in [TIME]
+ anyio==4.3.0
+ bird-feeder==1.0.0 (from file://[WORKSPACE]/scripts/workspaces/albatross-root-workspace/packages/bird-feeder)
@ -284,9 +278,8 @@ fn test_albatross_root_workspace_albatross() {
----- stdout -----
----- stderr -----
Built 2 editables in [TIME]
Resolved 5 packages in [TIME]
Downloaded 3 packages in [TIME]
Downloaded 5 packages in [TIME]
Installed 5 packages in [TIME]
+ anyio==4.3.0
+ bird-feeder==1.0.0 (from file://[WORKSPACE]/scripts/workspaces/albatross-root-workspace/packages/bird-feeder)
@ -324,9 +317,8 @@ fn test_albatross_virtual_workspace() {
----- stdout -----
----- stderr -----
Built 2 editables in [TIME]
Resolved 5 packages in [TIME]
Downloaded 3 packages in [TIME]
Downloaded 5 packages in [TIME]
Installed 5 packages in [TIME]
+ anyio==4.3.0
+ bird-feeder==1.0.0 (from file://[WORKSPACE]/scripts/workspaces/albatross-virtual-workspace/packages/bird-feeder)

View file

@ -1,2 +0,0 @@
# Artifacts from the build process.
*.egg-info/

View file

@ -1,3 +0,0 @@
from setuptools import setup
setup(name="first-editable", version="0.0.1", install_requires=[])

View file

@ -1,3 +0,0 @@
# Artifacts from the build process.
*.egg-info/
build/

View file

@ -1,9 +0,0 @@
from setuptools import setup
setup(
name="second-editable",
version="0.0.1",
install_requires=[
"first-editable",
],
)