Allow prereleases, locals, and URLs in non-editable path requirements (#2671)

## Summary

This PR enables the resolver to "accept" URLs, prereleases, and local
version specifiers for direct dependencies of path dependencies. As a
result, `uv pip install .` and `uv pip install -e .` now behave
identically, in that neither has a restriction on URL dependencies and
the like.

Closes https://github.com/astral-sh/uv/issues/2643.
Closes https://github.com/astral-sh/uv/issues/1853.
This commit is contained in:
Charlie Marsh 2024-03-27 18:17:09 -04:00 committed by GitHub
parent 4b69ad4281
commit cf30932831
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 484 additions and 77 deletions

View file

@ -425,6 +425,14 @@ impl SourceDist {
dist => dist,
}
}
/// Returns the path to the source distribution, if if it's a local distribution.
pub fn as_path(&self) -> Option<&Path> {
match self {
Self::Path(dist) => Some(&dist.path),
_ => None,
}
}
}
impl Name for RegistryBuiltDist {

View file

@ -0,0 +1,35 @@
use pep508_rs::Requirement;
use uv_normalize::ExtraName;
/// A set of requirements as requested by a parent requirement.
///
/// For example, given `flask[dotenv]`, the `RequestedRequirements` would include the `dotenv`
/// extra, along with all of the requirements that are included in the `flask` distribution
/// including their unevaluated markers.
#[derive(Debug, Clone)]
pub struct RequestedRequirements {
/// The set of extras included on the originating requirement.
extras: Vec<ExtraName>,
/// The set of requirements that were requested by the originating requirement.
requirements: Vec<Requirement>,
}
impl RequestedRequirements {
/// Instantiate a [`RequestedRequirements`] with the given `extras` and `requirements`.
pub fn new(extras: Vec<ExtraName>, requirements: Vec<Requirement>) -> Self {
Self {
extras,
requirements,
}
}
/// Return the extras that were included on the originating requirement.
pub fn extras(&self) -> &[ExtraName] {
&self.extras
}
/// Return the requirements that were included on the originating requirement.
pub fn requirements(&self) -> &[Requirement] {
&self.requirements
}
}

View file

@ -385,6 +385,11 @@ impl Scheme {
_ => None,
}
}
/// Returns `true` if the scheme is a file scheme.
pub fn is_file(self) -> bool {
matches!(self, Self::File)
}
}
impl std::fmt::Display for Scheme {

View file

@ -1,9 +1,11 @@
pub use crate::lookahead::*;
pub use crate::resolver::*;
pub use crate::source_tree::*;
pub use crate::sources::*;
pub use crate::specification::*;
mod confirm;
mod lookahead;
mod pyproject;
mod resolver;
mod source_tree;

View file

@ -0,0 +1,104 @@
use std::sync::Arc;
use anyhow::{Context, Result};
use futures::{StreamExt, TryStreamExt};
use distribution_types::{BuildableSource, Dist};
use pep508_rs::{Requirement, VersionOrUrl};
use uv_client::RegistryClient;
use uv_distribution::{Reporter, SourceDistCachedBuilder};
use uv_types::{BuildContext, RequestedRequirements};
/// A resolver for resolving lookahead requirements from local dependencies.
///
/// The resolver extends certain privileges to "first-party" requirements. For example, first-party
/// requirements are allowed to contain direct URL references, local version specifiers, and more.
///
/// We make an exception for transitive requirements of _local_ dependencies. For example,
/// `pip install .` should treat the dependencies of `.` as if they were first-party dependencies.
/// This matches our treatment of editable installs (`pip install -e .`).
///
/// The lookahead resolver resolves requirements for local dependencies, so that the resolver can
/// treat them as first-party dependencies for the purpose of analyzing their specifiers.
pub struct LookaheadResolver<'a> {
/// The requirements for the project.
requirements: &'a [Requirement],
/// The reporter to use when building source distributions.
reporter: Option<Arc<dyn Reporter>>,
}
impl<'a> LookaheadResolver<'a> {
/// Instantiate a new [`LookaheadResolver`] for a given set of `source_trees`.
pub fn new(requirements: &'a [Requirement]) -> Self {
Self {
requirements,
reporter: None,
}
}
/// Set the [`Reporter`] to use for this resolver.
#[must_use]
pub fn with_reporter(self, reporter: impl Reporter + 'static) -> Self {
let reporter: Arc<dyn Reporter> = Arc::new(reporter);
Self {
reporter: Some(reporter),
..self
}
}
/// Resolve the requirements from the provided source trees.
pub async fn resolve<T: BuildContext>(
self,
context: &T,
client: &RegistryClient,
) -> Result<Vec<RequestedRequirements>> {
let requirements: Vec<_> = futures::stream::iter(self.requirements.iter())
.map(|requirement| async { self.lookahead(requirement, context, client).await })
.buffered(50)
.try_collect()
.await?;
Ok(requirements.into_iter().flatten().collect())
}
/// Infer the package name for a given "unnamed" requirement.
async fn lookahead<T: BuildContext>(
&self,
requirement: &Requirement,
context: &T,
client: &RegistryClient,
) -> Result<Option<RequestedRequirements>> {
// Determine whether the requirement represents a local distribution.
let Some(VersionOrUrl::Url(url)) = requirement.version_or_url.as_ref() else {
return Ok(None);
};
// Convert to a buildable distribution.
let dist = Dist::from_url(requirement.name.clone(), url.clone())?;
// Only support source trees (and not, e.g., wheels).
let Dist::Source(source_dist) = &dist else {
return Ok(None);
};
if !source_dist.as_path().is_some_and(std::path::Path::is_dir) {
return Ok(None);
}
// Run the PEP 517 build process to extract metadata from the source distribution.
let builder = if let Some(reporter) = self.reporter.clone() {
SourceDistCachedBuilder::new(context, client).with_reporter(reporter)
} else {
SourceDistCachedBuilder::new(context, client)
};
let metadata = builder
.download_and_build_metadata(&BuildableSource::Dist(source_dist))
.await
.context("Failed to build source distribution")?;
// Return the requirements from the metadata.
Ok(Some(RequestedRequirements::new(
requirement.extras.clone(),
metadata.requires_dist,
)))
}
}

View file

@ -2,18 +2,44 @@ use distribution_types::LocalEditable;
use pep508_rs::Requirement;
use pypi_types::Metadata23;
use uv_normalize::PackageName;
use uv_types::RequestedRequirements;
use crate::preferences::Preference;
/// A manifest of requirements, constraints, and preferences.
#[derive(Clone, Debug)]
pub struct Manifest {
/// The direct requirements for the project.
pub(crate) requirements: Vec<Requirement>,
/// The constraints for the project.
pub(crate) constraints: Vec<Requirement>,
/// The overrides for the project.
pub(crate) overrides: Vec<Requirement>,
/// The preferences for the project.
///
/// These represent "preferred" versions of a given package. For example, they may be the
/// versions that are already installed in the environment, or already pinned in an existing
/// lockfile.
pub(crate) preferences: Vec<Preference>,
/// 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<(LocalEditable, Metadata23)>,
/// The lookahead requirements for the project.
///
/// These represent transitive dependencies that should be incorporated when making
/// determinations around "allowed" versions (for example, "allowed" URLs or "allowed"
/// pre-release versions).
pub(crate) lookaheads: Vec<RequestedRequirements>,
}
impl Manifest {
@ -24,6 +50,7 @@ impl Manifest {
preferences: Vec<Preference>,
project: Option<PackageName>,
editables: Vec<(LocalEditable, Metadata23)>,
lookaheads: Vec<RequestedRequirements>,
) -> Self {
Self {
requirements,
@ -32,6 +59,7 @@ impl Manifest {
preferences,
project,
editables,
lookaheads,
}
}
@ -43,6 +71,7 @@ impl Manifest {
preferences: Vec::new(),
project: None,
editables: Vec::new(),
lookaheads: Vec::new(),
}
}
}

View file

@ -66,6 +66,11 @@ impl PreReleaseStrategy {
.chain(manifest.constraints.iter())
.chain(manifest.overrides.iter())
.filter(|requirement| requirement.evaluate_markers(markers, &[]))
.chain(manifest.lookaheads.iter().flat_map(|lookahead| {
lookahead.requirements().iter().filter(|requirement| {
requirement.evaluate_markers(markers, lookahead.extras())
})
}))
.chain(manifest.editables.iter().flat_map(|(editable, metadata)| {
metadata.requires_dist.iter().filter(|requirement| {
requirement.evaluate_markers(markers, &editable.extras)
@ -95,6 +100,11 @@ impl PreReleaseStrategy {
.chain(manifest.constraints.iter())
.chain(manifest.overrides.iter())
.filter(|requirement| requirement.evaluate_markers(markers, &[]))
.chain(manifest.lookaheads.iter().flat_map(|lookahead| {
lookahead.requirements().iter().filter(|requirement| {
requirement.evaluate_markers(markers, lookahead.extras())
})
}))
.chain(manifest.editables.iter().flat_map(|(editable, metadata)| {
metadata.requires_dist.iter().filter(|requirement| {
requirement.evaluate_markers(markers, &editable.extras)

View file

@ -41,11 +41,16 @@ impl ResolutionStrategy {
ResolutionMode::Highest => Self::Highest,
ResolutionMode::Lowest => Self::Lowest,
ResolutionMode::LowestDirect => Self::LowestDirect(
// Consider `requirements` and dependencies of `editables` to be "direct" dependencies.
// Consider `requirements` and dependencies of any local requirements to be "direct" dependencies.
manifest
.requirements
.iter()
.filter(|requirement| requirement.evaluate_markers(markers, &[]))
.chain(manifest.lookaheads.iter().flat_map(|lookahead| {
lookahead.requirements().iter().filter(|requirement| {
requirement.evaluate_markers(markers, lookahead.extras())
})
}))
.chain(manifest.editables.iter().flat_map(|(editable, metadata)| {
metadata.requires_dist.iter().filter(|requirement| {
requirement.evaluate_markers(markers, &editable.extras)

View file

@ -24,28 +24,23 @@ impl Locals {
// Add all direct requirements and constraints. There's no need to look for conflicts,
// since conflicting versions will be tracked upstream.
for requirement in manifest
for requirement in
manifest
.requirements
.iter()
.chain(manifest.constraints.iter())
.chain(manifest.overrides.iter())
.filter(|requirement| requirement.evaluate_markers(markers, &[]))
.chain(
manifest
.constraints
.iter()
.filter(|requirement| requirement.evaluate_markers(markers, &[])),
)
.chain(manifest.editables.iter().flat_map(|(editable, metadata)| {
metadata
.requires_dist
.iter()
.filter(|requirement| requirement.evaluate_markers(markers, &editable.extras))
.chain(manifest.lookaheads.iter().flat_map(|lookahead| {
lookahead.requirements().iter().filter(|requirement| {
requirement.evaluate_markers(markers, lookahead.extras())
})
}))
.chain(manifest.editables.iter().flat_map(|(editable, metadata)| {
metadata.requires_dist.iter().filter(|requirement| {
requirement.evaluate_markers(markers, &editable.extras)
})
}))
.chain(
manifest
.overrides
.iter()
.filter(|requirement| requirement.evaluate_markers(markers, &[])),
)
{
if let Some(version_or_url) = requirement.version_or_url.as_ref() {
for local in iter_locals(version_or_url) {

View file

@ -23,16 +23,39 @@ impl Urls {
let mut required: FxHashMap<PackageName, VerbatimUrl> = FxHashMap::default();
let mut allowed: FxHashMap<VerbatimUrl, VerbatimUrl> = FxHashMap::default();
// Add any lookahead requirements. If there are any conflicts, return an error.
for lookahead in &manifest.lookaheads {
for requirement in lookahead
.requirements()
.iter()
.filter(|requirement| requirement.evaluate_markers(markers, lookahead.extras()))
{
if let Some(pep508_rs::VersionOrUrl::Url(url)) = &requirement.version_or_url {
if let Some(previous) = required.insert(requirement.name.clone(), url.clone()) {
if !is_equal(&previous, url) {
if is_precise(&previous, url) {
debug!("Assuming {url} is a precise variant of {previous}");
allowed.insert(url.clone(), previous);
} else {
return Err(ResolveError::ConflictingUrlsDirect(
requirement.name.clone(),
previous.verbatim().to_string(),
url.verbatim().to_string(),
));
}
}
}
}
}
}
// Add all direct requirements and constraints. If there are any conflicts, return an error.
for requirement in manifest
.requirements
.iter()
.chain(manifest.constraints.iter())
.filter(|requirement| requirement.evaluate_markers(markers, &[]))
{
if !requirement.evaluate_markers(markers, &[]) {
continue;
}
if let Some(pep508_rs::VersionOrUrl::Url(url)) = &requirement.version_or_url {
if let Some(previous) = required.insert(requirement.name.clone(), url.clone()) {
if is_equal(&previous, url) {
@ -74,11 +97,11 @@ impl Urls {
}
}
for requirement in &metadata.requires_dist {
if !requirement.evaluate_markers(markers, &editable.extras) {
continue;
}
for requirement in metadata
.requires_dist
.iter()
.filter(|requirement| requirement.evaluate_markers(markers, &editable.extras))
{
if let Some(pep508_rs::VersionOrUrl::Url(url)) = &requirement.version_or_url {
if let Some(previous) = required.insert(requirement.name.clone(), url.clone()) {
if !is_equal(&previous, url) {
@ -100,11 +123,11 @@ impl Urls {
// Add any overrides. Conflicts here are fine, as the overrides are meant to be
// authoritative.
for requirement in &manifest.overrides {
if !requirement.evaluate_markers(markers, &[]) {
continue;
}
for requirement in manifest
.overrides
.iter()
.filter(|requirement| requirement.evaluate_markers(markers, &[]))
{
if let Some(pep508_rs::VersionOrUrl::Url(url)) = &requirement.version_or_url {
required.insert(requirement.name.clone(), url.clone());
}

View file

@ -1,7 +1,7 @@
use rustc_hash::{FxHashMap, FxHashSet};
use pep440_rs::Version;
use pep508_rs::MarkerEnvironment;
use pep508_rs::{MarkerEnvironment, VersionOrUrl};
use uv_normalize::PackageName;
use crate::preferences::Preference;
@ -15,22 +15,26 @@ pub struct AllowedYanks(FxHashMap<PackageName, FxHashSet<Version>>);
impl AllowedYanks {
pub fn from_manifest(manifest: &Manifest, markers: &MarkerEnvironment) -> Self {
let mut allowed_yanks = FxHashMap::<PackageName, FxHashSet<Version>>::default();
for requirement in manifest
for requirement in
manifest
.requirements
.iter()
.chain(manifest.constraints.iter())
.chain(manifest.overrides.iter())
.chain(manifest.preferences.iter().map(Preference::requirement))
.filter(|requirement| requirement.evaluate_markers(markers, &[]))
.chain(manifest.lookaheads.iter().flat_map(|lookahead| {
lookahead.requirements().iter().filter(|requirement| {
requirement.evaluate_markers(markers, lookahead.extras())
})
}))
.chain(manifest.editables.iter().flat_map(|(editable, metadata)| {
metadata
.requires_dist
.iter()
.filter(|requirement| requirement.evaluate_markers(markers, &editable.extras))
metadata.requires_dist.iter().filter(|requirement| {
requirement.evaluate_markers(markers, &editable.extras)
})
}))
{
let Some(pep508_rs::VersionOrUrl::VersionSpecifier(specifiers)) =
&requirement.version_or_url
let Some(VersionOrUrl::VersionSpecifier(specifiers)) = &requirement.version_or_url
else {
continue;
};

View file

@ -271,6 +271,7 @@ async fn black_mypy_extensions() -> Result<()> {
vec![],
None,
vec![],
vec![],
);
let options = OptionsBuilder::new()
.exclude_newer(Some(*EXCLUDE_NEWER))
@ -306,6 +307,7 @@ async fn black_mypy_extensions_extra() -> Result<()> {
vec![],
None,
vec![],
vec![],
);
let options = OptionsBuilder::new()
.exclude_newer(Some(*EXCLUDE_NEWER))
@ -341,6 +343,7 @@ async fn black_flake8() -> Result<()> {
vec![],
None,
vec![],
vec![],
);
let options = OptionsBuilder::new()
.exclude_newer(Some(*EXCLUDE_NEWER))
@ -430,6 +433,7 @@ async fn black_respect_preference() -> Result<()> {
)?)],
None,
vec![],
vec![],
);
let options = OptionsBuilder::new()
.exclude_newer(Some(*EXCLUDE_NEWER))
@ -465,6 +469,7 @@ async fn black_ignore_preference() -> Result<()> {
)?)],
None,
vec![],
vec![],
);
let options = OptionsBuilder::new()
.exclude_newer(Some(*EXCLUDE_NEWER))

View file

@ -4,6 +4,7 @@ pub use config_settings::*;
pub use downloads::*;
pub use name_specifiers::*;
pub use package_options::*;
pub use requirements::*;
pub use traits::*;
mod build_options;
@ -11,4 +12,5 @@ mod config_settings;
mod downloads;
mod name_specifiers;
mod package_options;
mod requirements;
mod traits;

View file

@ -0,0 +1,35 @@
use pep508_rs::Requirement;
use uv_normalize::ExtraName;
/// A set of requirements as requested by a parent requirement.
///
/// For example, given `flask[dotenv]`, the `RequestedRequirements` would include the `dotenv`
/// extra, along with all of the requirements that are included in the `flask` distribution
/// including their unevaluated markers.
#[derive(Debug, Clone)]
pub struct RequestedRequirements {
/// The set of extras included on the originating requirement.
extras: Vec<ExtraName>,
/// The set of requirements that were requested by the originating requirement.
requirements: Vec<Requirement>,
}
impl RequestedRequirements {
/// Instantiate a [`RequestedRequirements`] with the given `extras` and `requirements`.
pub fn new(extras: Vec<ExtraName>, requirements: Vec<Requirement>) -> Self {
Self {
extras,
requirements,
}
}
/// Return the extras that were included on the originating requirement.
pub fn extras(&self) -> &[ExtraName] {
&self.extras
}
/// Return the requirements that were included on the originating requirement.
pub fn requirements(&self) -> &[Requirement] {
&self.requirements
}
}

View file

@ -28,8 +28,8 @@ use uv_installer::{Downloader, NoBinary};
use uv_interpreter::{find_best_python, PythonEnvironment, PythonVersion};
use uv_normalize::{ExtraName, PackageName};
use uv_requirements::{
upgrade::read_lockfile, ExtrasSpecification, NamedRequirementsResolver, RequirementsSource,
RequirementsSpecification, SourceTreeResolver,
upgrade::read_lockfile, ExtrasSpecification, LookaheadResolver, NamedRequirementsResolver,
RequirementsSource, RequirementsSpecification, SourceTreeResolver,
};
use uv_resolver::{
AnnotationStyle, DependencyMode, DisplayResolutionGraph, InMemoryIndex, Manifest,
@ -274,6 +274,12 @@ pub(crate) async fn pip_compile(
requirements
};
// Determine any lookahead requirements.
let lookaheads = LookaheadResolver::new(&requirements)
.with_reporter(ResolverReporter::from(printer))
.resolve(&build_dispatch, &client)
.await?;
// Build the editables and add their requirements
let editable_metadata = if editables.is_empty() {
Vec::new()
@ -338,6 +344,7 @@ pub(crate) async fn pip_compile(
preferences,
project,
editable_metadata,
lookaheads,
);
let options = OptionsBuilder::new()

View file

@ -1,7 +1,6 @@
use std::collections::HashSet;
use std::fmt::Write;
use std::path::Path;
use std::time::Instant;
use anstream::eprint;
use anyhow::{anyhow, Context, Result};
@ -34,8 +33,8 @@ use uv_installer::{
use uv_interpreter::{Interpreter, PythonEnvironment};
use uv_normalize::PackageName;
use uv_requirements::{
ExtrasSpecification, NamedRequirementsResolver, RequirementsSource, RequirementsSpecification,
SourceTreeResolver,
ExtrasSpecification, LookaheadResolver, NamedRequirementsResolver, RequirementsSource,
RequirementsSpecification, SourceTreeResolver,
};
use uv_resolver::{
DependencyMode, InMemoryIndex, Manifest, Options, OptionsBuilder, PreReleaseMode, Preference,
@ -82,7 +81,7 @@ pub(crate) async fn pip_install(
dry_run: bool,
printer: Printer,
) -> Result<ExitStatus> {
let start = Instant::now();
let start = std::time::Instant::now();
let client_builder = BaseClientBuilder::new()
.connectivity(connectivity)
.native_tls(native_tls)
@ -437,7 +436,7 @@ async fn build_editables(
build_dispatch: &BuildDispatch<'_>,
printer: Printer,
) -> Result<Vec<BuiltEditable>, Error> {
let start = Instant::now();
let start = std::time::Instant::now();
let downloader = Downloader::new(cache, tags, client, build_dispatch)
.with_reporter(DownloadReporter::from(printer).with_length(editables.len() as u64));
@ -547,6 +546,12 @@ async fn resolve(
})
.collect();
// Determine any lookahead requirements.
let lookaheads = LookaheadResolver::new(&requirements)
.with_reporter(ResolverReporter::from(printer))
.resolve(build_dispatch, client)
.await?;
// Create a manifest of the requirements.
let manifest = Manifest::new(
requirements,
@ -555,6 +560,7 @@ async fn resolve(
preferences,
project,
editables,
lookaheads,
);
// Resolve the dependencies.
@ -674,7 +680,7 @@ async fn install(
let wheels = if remote.is_empty() {
vec![]
} else {
let start = Instant::now();
let start = std::time::Instant::now();
let downloader = Downloader::new(cache, tags, client, build_dispatch)
.with_reporter(DownloadReporter::from(printer).with_length(remote.len() as u64));
@ -796,7 +802,7 @@ async fn install(
fn report_dry_run(
resolution: &Resolution,
plan: Plan,
start: Instant,
start: std::time::Instant,
printer: Printer,
) -> Result<(), Error> {
let Plan {

View file

@ -1691,7 +1691,7 @@ fn incompatible_narrowed_url_dependency() -> Result<()> {
Ok(())
}
/// Request `transitive_url_dependency`, which depends on `https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl`.
/// Request `hatchling_editable`, which depends on `https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl`.
/// Since this URL isn't declared upfront, we should reject it.
#[test]
#[cfg(feature = "git")]
@ -1699,12 +1699,10 @@ fn disallowed_transitive_url_dependency() -> Result<()> {
let context = TestContext::new("3.12");
let requirements_in = context.temp_dir.child("requirements.in");
requirements_in.write_str("hatchling_editable @ ${HATCHLING}")?;
requirements_in.write_str("hatchling_editable @ https://github.com/astral-sh/uv/files/14762645/hatchling_editable.zip")?;
let hatchling_path = current_dir()?.join("../../scripts/packages/hatchling_editable");
uv_snapshot!(context.compile()
.arg("requirements.in")
.env("HATCHLING", hatchling_path.as_os_str()), @r###"
.arg("requirements.in"), @r###"
success: false
exit_code: 2
----- stdout -----
@ -1725,23 +1723,21 @@ fn allowed_transitive_url_dependency() -> Result<()> {
let context = TestContext::new("3.12");
let requirements_in = context.temp_dir.child("requirements.in");
requirements_in.write_str("hatchling_editable @ ${HATCHLING}")?;
requirements_in.write_str("hatchling_editable @ https://github.com/astral-sh/uv/files/14762645/hatchling_editable.zip")?;
let constraints_txt = context.temp_dir.child("constraints.txt");
constraints_txt.write_str("iniconfig @ git+https://github.com/pytest-dev/iniconfig@9cae43103df70bac6fde7b9f35ad11a9f1be0cb4")?;
let hatchling_path = current_dir()?.join("../../scripts/packages/hatchling_editable");
uv_snapshot!(context.compile()
.arg("requirements.in")
.arg("--constraint")
.arg("constraints.txt")
.env("HATCHLING", hatchling_path.as_os_str()), @r###"
.arg("constraints.txt"), @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 --constraint constraints.txt
hatchling-editable @ ${HATCHLING}
hatchling-editable @ https://github.com/astral-sh/uv/files/14762645/hatchling_editable.zip
iniconfig @ git+https://github.com/pytest-dev/iniconfig@9cae43103df70bac6fde7b9f35ad11a9f1be0cb4
# via hatchling-editable
@ -1762,23 +1758,21 @@ fn allowed_transitive_canonical_url_dependency() -> Result<()> {
let context = TestContext::new("3.12");
let requirements_in = context.temp_dir.child("requirements.in");
requirements_in.write_str("hatchling_editable @ ${HATCHLING}")?;
requirements_in.write_str("hatchling_editable @ https://github.com/astral-sh/uv/files/14762645/hatchling_editable.zip")?;
let constraints_txt = context.temp_dir.child("constraints.txt");
constraints_txt.write_str("iniconfig @ git+https://github.com/pytest-dev/iniconfig.git@9cae43103df70bac6fde7b9f35ad11a9f1be0cb4")?;
let hatchling_path = current_dir()?.join("../../scripts/packages/hatchling_editable");
uv_snapshot!(context.compile()
.arg("requirements.in")
.arg("--constraint")
.arg("constraints.txt")
.env("HATCHLING", hatchling_path.as_os_str()), @r###"
.arg("constraints.txt"), @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 --constraint constraints.txt
hatchling-editable @ ${HATCHLING}
hatchling-editable @ https://github.com/astral-sh/uv/files/14762645/hatchling_editable.zip
iniconfig @ git+https://github.com/pytest-dev/iniconfig.git@9cae43103df70bac6fde7b9f35ad11a9f1be0cb4
# via hatchling-editable
@ -1790,6 +1784,37 @@ fn allowed_transitive_canonical_url_dependency() -> Result<()> {
Ok(())
}
/// Request `hatchling_editable`, which depends on `https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl`.
/// Since `hatchling_editable` is a path (local) dependency, we should accept it.
#[test]
#[cfg(feature = "git")]
fn allowed_transitive_url_path_dependency() -> Result<()> {
let context = TestContext::new("3.12");
let requirements_in = context.temp_dir.child("requirements.in");
requirements_in.write_str("hatchling_editable @ ${HATCH_PATH}")?;
let hatchling_path = current_dir()?.join("../../scripts/packages/hatchling_editable");
uv_snapshot!(context.compile()
.arg("requirements.in")
.env("HATCH_PATH", hatchling_path.as_os_str()), @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
hatchling-editable @ ${HATCH_PATH}
iniconfig @ git+https://github.com/pytest-dev/iniconfig@9cae43103df70bac6fde7b9f35ad11a9f1be0cb4
# via hatchling-editable
----- stderr -----
Resolved 2 packages in [TIME]
"###
);
Ok(())
}
/// Resolve packages from all optional dependency groups in a `pyproject.toml` file.
#[test]
fn compile_pyproject_toml_all_extras() -> Result<()> {
@ -6328,3 +6353,110 @@ fn pendulum_no_tzdata_on_windows() -> Result<()> {
Ok(())
}
/// Allow pre-releases for dependencies of source path requirements.
#[test]
fn pre_release_path_requirement() -> Result<()> {
let context = TestContext::new("3.12");
// Create an a package that requires a pre-release version of `flask`.
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"[project]
name = "example"
version = "0.0.0"
dependencies = [
"flask==2.0.0rc1"
]
requires-python = ">3.8"
"#,
)?;
// Write to a requirements file.
let requirements_in = context.temp_dir.child("requirements.in");
requirements_in.write_str(".")?;
uv_snapshot!(context.compile()
.arg("requirements.in"), @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
click==8.1.7
# via flask
example @ .
flask==2.0.0rc1
# via example
itsdangerous==2.1.2
# via flask
jinja2==3.1.3
# via flask
markupsafe==2.1.5
# via
# jinja2
# werkzeug
werkzeug==3.0.1
# via flask
----- stderr -----
Resolved 7 packages in [TIME]
"###
);
Ok(())
}
/// Allow pre-releases for dependencies of editable requirements.
#[test]
fn pre_release_editable_requirement() -> Result<()> {
let context = TestContext::new("3.12");
// Create an a package that requires a pre-release version of `flask`.r
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"[project]
name = "example"
version = "0.0.0"
dependencies = [
"flask==2.0.0rc1"
]
requires-python = ">3.8"
"#,
)?;
// Write to a requirements file.
let requirements_in = context.temp_dir.child("requirements.in");
requirements_in.write_str("-e .")?;
uv_snapshot!( context.compile()
.arg("requirements.in"), @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
-e .
click==8.1.7
# via flask
flask==2.0.0rc1
# via example
itsdangerous==2.1.2
# via flask
jinja2==3.1.3
# via flask
markupsafe==2.1.5
# via
# jinja2
# werkzeug
werkzeug==3.0.1
# via flask
----- stderr -----
Built 1 editable in [TIME]
Resolved 7 packages in [TIME]
"###
);
Ok(())
}