mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-03 18:38:21 +00:00
Add pylock.toml
to uv pip install
and uv pip sync
(#12992)
## Summary We accept `pylock.toml` as a requirements file (e.g., `uv sync pylock.toml` or `uv pip install -r pylock.toml`). When you provide a `pylock.toml` file, we don't allow you to provide other requirements, or constraints, etc. And you can only provide one `pylock.toml` file, not multiple. We might want to remove this from `uv pip install` for now, since `pip` may end up with a different interface (whereas `uv pip sync` is already specific to uv), and most of the arguments aren't applicable (like `--resolution`, etc.). Regardless, it's behind `--preview` for both commands.
This commit is contained in:
parent
05c40921cc
commit
e089c42e43
10 changed files with 1572 additions and 148 deletions
|
@ -19,7 +19,7 @@ pub struct Resolution {
|
|||
}
|
||||
|
||||
impl Resolution {
|
||||
/// Create a new resolution from the given pinned packages.
|
||||
/// Create a [`Resolution`] from the given pinned packages.
|
||||
pub fn new(graph: petgraph::graph::DiGraph<Node, Edge>) -> Self {
|
||||
Self {
|
||||
graph,
|
||||
|
@ -208,17 +208,6 @@ pub enum Edge {
|
|||
Dev(GroupName, MarkerTree),
|
||||
}
|
||||
|
||||
impl Edge {
|
||||
/// Return the [`MarkerTree`] for this edge.
|
||||
pub fn marker(&self) -> &MarkerTree {
|
||||
match self {
|
||||
Self::Prod(marker) => marker,
|
||||
Self::Optional(_, marker) => marker,
|
||||
Self::Dev(_, marker) => marker,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ResolvedDist> for RequirementSource {
|
||||
fn from(resolved_dist: &ResolvedDist) -> Self {
|
||||
match resolved_dist {
|
||||
|
|
|
@ -13,6 +13,8 @@ pub enum RequirementsSource {
|
|||
Package(RequirementsTxtRequirement),
|
||||
/// An editable path was provided on the command line (e.g., `pip install -e ../flask`).
|
||||
Editable(RequirementsTxtRequirement),
|
||||
/// Dependencies were provided via a `pylock.toml` file.
|
||||
PylockToml(PathBuf),
|
||||
/// Dependencies were provided via a `requirements.txt` file (e.g., `pip install -r requirements.txt`).
|
||||
RequirementsTxt(PathBuf),
|
||||
/// Dependencies were provided via a `pyproject.toml` file (e.g., `pip-compile pyproject.toml`).
|
||||
|
@ -39,6 +41,11 @@ impl RequirementsSource {
|
|||
Self::SetupCfg(path)
|
||||
} else if path.ends_with("environment.yml") {
|
||||
Self::EnvironmentYml(path)
|
||||
} else if path
|
||||
.file_name()
|
||||
.is_some_and(|file_name| file_name.to_str().is_some_and(is_pylock_toml))
|
||||
{
|
||||
Self::PylockToml(path)
|
||||
} else {
|
||||
Self::RequirementsTxt(path)
|
||||
}
|
||||
|
@ -46,12 +53,20 @@ impl RequirementsSource {
|
|||
|
||||
/// Parse a [`RequirementsSource`] from a `requirements.txt` file.
|
||||
pub fn from_requirements_txt(path: PathBuf) -> Self {
|
||||
for filename in ["pyproject.toml", "setup.py", "setup.cfg"] {
|
||||
if path.ends_with(filename) {
|
||||
for file_name in ["pyproject.toml", "setup.py", "setup.cfg"] {
|
||||
if path.ends_with(file_name) {
|
||||
warn_user!(
|
||||
"The file `{}` appears to be a `{}` file, but requirements must be specified in `requirements.txt` format.",
|
||||
path.user_display(),
|
||||
filename
|
||||
file_name
|
||||
);
|
||||
}
|
||||
}
|
||||
if let Some(file_name) = path.file_name() {
|
||||
if file_name.to_str().is_some_and(is_pylock_toml) {
|
||||
warn_user!(
|
||||
"The file `{}` appears to be a `pylock.toml` file, but requirements must be specified in `requirements.txt` format.",
|
||||
path.user_display(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -60,12 +75,20 @@ impl RequirementsSource {
|
|||
|
||||
/// Parse a [`RequirementsSource`] from a `constraints.txt` file.
|
||||
pub fn from_constraints_txt(path: PathBuf) -> Self {
|
||||
for filename in ["pyproject.toml", "setup.py", "setup.cfg"] {
|
||||
if path.ends_with(filename) {
|
||||
for file_name in ["pyproject.toml", "setup.py", "setup.cfg"] {
|
||||
if path.ends_with(file_name) {
|
||||
warn_user!(
|
||||
"The file `{}` appears to be a `{}` file, but constraints must be specified in `requirements.txt` format.",
|
||||
path.user_display(),
|
||||
filename
|
||||
file_name
|
||||
);
|
||||
}
|
||||
}
|
||||
if let Some(file_name) = path.file_name() {
|
||||
if file_name.to_str().is_some_and(is_pylock_toml) {
|
||||
warn_user!(
|
||||
"The file `{}` appears to be a `pylock.toml` file, but constraints must be specified in `requirements.txt` format.",
|
||||
path.user_display(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -74,12 +97,20 @@ impl RequirementsSource {
|
|||
|
||||
/// Parse a [`RequirementsSource`] from an `overrides.txt` file.
|
||||
pub fn from_overrides_txt(path: PathBuf) -> Self {
|
||||
for filename in ["pyproject.toml", "setup.py", "setup.cfg"] {
|
||||
if path.ends_with(filename) {
|
||||
for file_name in ["pyproject.toml", "setup.py", "setup.cfg"] {
|
||||
if path.ends_with(file_name) {
|
||||
warn_user!(
|
||||
"The file `{}` appears to be a `{}` file, but overrides must be specified in `requirements.txt` format.",
|
||||
path.user_display(),
|
||||
filename
|
||||
file_name
|
||||
);
|
||||
}
|
||||
}
|
||||
if let Some(file_name) = path.file_name() {
|
||||
if file_name.to_str().is_some_and(is_pylock_toml) {
|
||||
warn_user!(
|
||||
"The file `{}` appears to be a `pylock.toml` file, but overrides must be specified in `requirements.txt` format.",
|
||||
path.user_display(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -110,7 +141,10 @@ impl RequirementsSource {
|
|||
|
||||
// Similarly, if the user provided a `pyproject.toml` file without `-r` (as in
|
||||
// `uv pip install pyproject.toml`), prompt them to correct it.
|
||||
if (name == "pyproject.toml" || name == "setup.py" || name == "setup.cfg")
|
||||
if (name == "pyproject.toml"
|
||||
|| name == "setup.py"
|
||||
|| name == "setup.cfg"
|
||||
|| is_pylock_toml(name))
|
||||
&& Path::new(&name).is_file()
|
||||
{
|
||||
let term = Term::stderr();
|
||||
|
@ -155,7 +189,10 @@ impl RequirementsSource {
|
|||
|
||||
// Similarly, if the user provided a `pyproject.toml` file without `--with-requirements` (as in
|
||||
// `uvx --with pyproject.toml ruff`), prompt them to correct it.
|
||||
if (name == "pyproject.toml" || name == "setup.py" || name == "setup.cfg")
|
||||
if (name == "pyproject.toml"
|
||||
|| name == "setup.py"
|
||||
|| name == "setup.cfg"
|
||||
|| is_pylock_toml(name))
|
||||
&& Path::new(&name).is_file()
|
||||
{
|
||||
let term = Term::stderr();
|
||||
|
@ -217,7 +254,8 @@ impl std::fmt::Display for RequirementsSource {
|
|||
match self {
|
||||
Self::Package(package) => write!(f, "{package:?}"),
|
||||
Self::Editable(path) => write!(f, "-e {path:?}"),
|
||||
Self::RequirementsTxt(path)
|
||||
Self::PylockToml(path)
|
||||
| Self::RequirementsTxt(path)
|
||||
| Self::PyprojectToml(path)
|
||||
| Self::SetupPy(path)
|
||||
| Self::SetupCfg(path)
|
||||
|
|
|
@ -61,6 +61,8 @@ pub struct RequirementsSpecification {
|
|||
pub constraints: Vec<NameRequirementSpecification>,
|
||||
/// The overrides for the project.
|
||||
pub overrides: Vec<UnresolvedRequirementSpecification>,
|
||||
/// The `pylock.toml` file from which to extract the resolution.
|
||||
pub pylock: Option<PathBuf>,
|
||||
/// The source trees from which to extract requirements.
|
||||
pub source_trees: Vec<PathBuf>,
|
||||
/// The groups to use for `source_trees`
|
||||
|
@ -190,6 +192,16 @@ impl RequirementsSpecification {
|
|||
..Self::default()
|
||||
}
|
||||
}
|
||||
RequirementsSource::PylockToml(path) => {
|
||||
if !path.is_file() {
|
||||
return Err(anyhow::anyhow!("File not found: `{}`", path.user_display()));
|
||||
}
|
||||
|
||||
Self {
|
||||
pylock: Some(path.clone()),
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
RequirementsSource::SourceTree(path) => {
|
||||
if !path.is_dir() {
|
||||
return Err(anyhow::anyhow!(
|
||||
|
@ -231,7 +243,66 @@ impl RequirementsSpecification {
|
|||
) -> Result<Self> {
|
||||
let mut spec = Self::default();
|
||||
|
||||
// Resolve sources into specifications so we know their `source_tree`s∂
|
||||
// Disallow `pylock.toml` files as constraints.
|
||||
if let Some(pylock_toml) = constraints.iter().find_map(|source| {
|
||||
if let RequirementsSource::PylockToml(path) = source {
|
||||
Some(path)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}) {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Cannot use `{}` as a constraint file",
|
||||
pylock_toml.user_display()
|
||||
));
|
||||
}
|
||||
|
||||
// Disallow `pylock.toml` files as overrides.
|
||||
if let Some(pylock_toml) = overrides.iter().find_map(|source| {
|
||||
if let RequirementsSource::PylockToml(path) = source {
|
||||
Some(path)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}) {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Cannot use `{}` as an override file",
|
||||
pylock_toml.user_display()
|
||||
));
|
||||
}
|
||||
|
||||
// If we have a `pylock.toml`, don't allow additional requirements, constraints, or
|
||||
// overrides.
|
||||
if requirements
|
||||
.iter()
|
||||
.any(|source| matches!(source, RequirementsSource::PylockToml(..)))
|
||||
{
|
||||
if requirements
|
||||
.iter()
|
||||
.any(|source| !matches!(source, RequirementsSource::PylockToml(..)))
|
||||
{
|
||||
return Err(anyhow::anyhow!(
|
||||
"Cannot specify additional requirements alongside a `pylock.toml` file",
|
||||
));
|
||||
}
|
||||
if !constraints.is_empty() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Cannot specify additional requirements with a `pylock.toml` file"
|
||||
));
|
||||
}
|
||||
if !overrides.is_empty() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Cannot specify constraints with a `pylock.toml` file"
|
||||
));
|
||||
}
|
||||
if !groups.is_empty() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Cannot specify groups with a `pylock.toml` file"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve sources into specifications so we know their `source_tree`.
|
||||
let mut requirement_sources = Vec::new();
|
||||
for source in requirements {
|
||||
let source = Self::from_source(source, client_builder).await?;
|
||||
|
@ -301,6 +372,18 @@ impl RequirementsSpecification {
|
|||
spec.extras.extend(source.extras);
|
||||
spec.source_trees.extend(source.source_trees);
|
||||
|
||||
// Allow at most one `pylock.toml`.
|
||||
if let Some(pylock) = source.pylock {
|
||||
if let Some(existing) = spec.pylock {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Multiple `pylock.toml` files specified: `{}` vs. `{}`",
|
||||
existing.user_display(),
|
||||
pylock.user_display()
|
||||
));
|
||||
}
|
||||
spec.pylock = Some(pylock);
|
||||
}
|
||||
|
||||
// Use the first project name discovered.
|
||||
if spec.project.is_none() {
|
||||
spec.project = source.project;
|
||||
|
|
|
@ -1,22 +1,76 @@
|
|||
use jiff::tz::TimeZone;
|
||||
use jiff::Timestamp;
|
||||
use std::borrow::Cow;
|
||||
use std::ffi::OsStr;
|
||||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use jiff::civil::{DateTime, Time};
|
||||
use jiff::tz::{Offset, TimeZone};
|
||||
use jiff::{civil, Timestamp};
|
||||
use serde::Deserialize;
|
||||
use toml_edit::{value, Array, ArrayOfTables, Item, Table};
|
||||
use url::Url;
|
||||
|
||||
use uv_configuration::{DependencyGroupsWithDefaults, ExtrasSpecification, InstallOptions};
|
||||
use uv_distribution_types::{IndexUrl, RegistryBuiltWheel, RemoteSource, SourceDist};
|
||||
use uv_distribution_filename::{
|
||||
BuildTag, DistExtension, ExtensionError, SourceDistExtension, SourceDistFilename,
|
||||
SourceDistFilenameError, WheelFilename, WheelFilenameError,
|
||||
};
|
||||
use uv_distribution_types::{
|
||||
BuiltDist, DirectUrlBuiltDist, DirectUrlSourceDist, DirectorySourceDist, Dist, Edge,
|
||||
FileLocation, GitSourceDist, IndexUrl, Node, PathBuiltDist, PathSourceDist, RegistryBuiltDist,
|
||||
RegistryBuiltWheel, RegistrySourceDist, RemoteSource, Resolution, ResolvedDist, SourceDist,
|
||||
ToUrlError, UrlString,
|
||||
};
|
||||
use uv_fs::{relative_to, PortablePathBuf};
|
||||
use uv_git_types::GitOid;
|
||||
use uv_git_types::{GitOid, GitReference, GitUrl, GitUrlParseError};
|
||||
use uv_normalize::{ExtraName, GroupName, PackageName};
|
||||
use uv_pep440::Version;
|
||||
use uv_pep508::MarkerTree;
|
||||
use uv_pypi_types::{Hashes, VcsKind};
|
||||
use uv_pep508::{MarkerEnvironment, MarkerTree, VerbatimUrl};
|
||||
use uv_platform_tags::{TagCompatibility, TagPriority, Tags};
|
||||
use uv_pypi_types::{HashDigests, Hashes, ParsedGitUrl, VcsKind};
|
||||
use uv_small_str::SmallString;
|
||||
|
||||
use crate::lock::export::ExportableRequirements;
|
||||
use crate::lock::{each_element_on_its_line_array, LockErrorKind, Source};
|
||||
use crate::lock::{each_element_on_its_line_array, Source};
|
||||
use crate::{Installable, LockError, RequiresPython};
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum PylockTomlError {
|
||||
#[error("`packages` entry for `{0}` must contain one of: `wheels`, `directory`, `archive`, `sdist`, or `vcs`")]
|
||||
MissingSource(PackageName),
|
||||
#[error("`packages.wheel` entry for `{0}` must have a `path` or `url`")]
|
||||
WheelMissingPathUrl(PackageName),
|
||||
#[error("`packages.sdist` entry for `{0}` must have a `path` or `url`")]
|
||||
SdistMissingPathUrl(PackageName),
|
||||
#[error("`packages.archive` entry for `{0}` must have a `path` or `url`")]
|
||||
ArchiveMissingPathUrl(PackageName),
|
||||
#[error("`packages.vcs` entry for `{0}` must have a `url` or `path`")]
|
||||
VcsMissingPathUrl(PackageName),
|
||||
#[error("URL must end in a valid wheel filename: `{0}`")]
|
||||
UrlMissingFilename(Url),
|
||||
#[error("Path must end in a valid wheel filename: `{0}`")]
|
||||
PathMissingFilename(Box<Path>),
|
||||
#[error("Failed to convert path to URL")]
|
||||
PathToUrl,
|
||||
#[error("Failed to convert URL to path")]
|
||||
UrlToPath,
|
||||
#[error(transparent)]
|
||||
WheelFilename(#[from] WheelFilenameError),
|
||||
#[error(transparent)]
|
||||
SourceDistFilename(#[from] SourceDistFilenameError),
|
||||
#[error(transparent)]
|
||||
ToUrl(#[from] ToUrlError),
|
||||
#[error(transparent)]
|
||||
GitUrlParse(#[from] GitUrlParseError),
|
||||
#[error(transparent)]
|
||||
LockError(#[from] LockError),
|
||||
#[error(transparent)]
|
||||
Extension(#[from] ExtensionError),
|
||||
#[error(transparent)]
|
||||
Jiff(#[from] jiff::Error),
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct PylockToml {
|
||||
|
@ -106,7 +160,9 @@ struct PylockTomlArchive {
|
|||
size: Option<u64>,
|
||||
#[serde(
|
||||
skip_serializing_if = "Option::is_none",
|
||||
serialize_with = "timestamp_to_toml_datetime"
|
||||
serialize_with = "timestamp_to_toml_datetime",
|
||||
deserialize_with = "timestamp_from_toml_datetime",
|
||||
default
|
||||
)]
|
||||
upload_time: Option<Timestamp>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
|
@ -125,7 +181,9 @@ struct PylockTomlSdist {
|
|||
path: Option<PortablePathBuf>,
|
||||
#[serde(
|
||||
skip_serializing_if = "Option::is_none",
|
||||
serialize_with = "timestamp_to_toml_datetime"
|
||||
serialize_with = "timestamp_to_toml_datetime",
|
||||
deserialize_with = "timestamp_from_toml_datetime",
|
||||
default
|
||||
)]
|
||||
upload_time: Option<Timestamp>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
|
@ -137,14 +195,16 @@ struct PylockTomlSdist {
|
|||
#[serde(rename_all = "kebab-case")]
|
||||
struct PylockTomlWheel {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
name: Option<SmallString>,
|
||||
name: Option<WheelFilename>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
url: Option<Url>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
path: Option<PortablePathBuf>,
|
||||
#[serde(
|
||||
skip_serializing_if = "Option::is_none",
|
||||
serialize_with = "timestamp_to_toml_datetime"
|
||||
serialize_with = "timestamp_to_toml_datetime",
|
||||
deserialize_with = "timestamp_from_toml_datetime",
|
||||
default
|
||||
)]
|
||||
upload_time: Option<Timestamp>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
|
@ -167,7 +227,7 @@ impl<'lock> PylockToml {
|
|||
dev: &DependencyGroupsWithDefaults,
|
||||
annotate: bool,
|
||||
install_options: &'lock InstallOptions,
|
||||
) -> Result<Self, LockError> {
|
||||
) -> Result<Self, PylockTomlError> {
|
||||
// Extract the packages from the lock file.
|
||||
let ExportableRequirements(mut nodes) = ExportableRequirements::from_lock(
|
||||
target,
|
||||
|
@ -216,14 +276,14 @@ impl<'lock> PylockToml {
|
|||
let wheels = package
|
||||
.wheels
|
||||
.iter()
|
||||
.map(|wheel| wheel.to_registry_dist(source, target.install_path()))
|
||||
.map(|wheel| wheel.to_registry_wheel(source, target.install_path()))
|
||||
.collect::<Result<Vec<RegistryBuiltWheel>, LockError>>()?;
|
||||
Some(
|
||||
wheels
|
||||
.into_iter()
|
||||
.map(|wheel| {
|
||||
let url =
|
||||
wheel.file.url.to_url().map_err(LockErrorKind::InvalidUrl)?;
|
||||
wheel.file.url.to_url().map_err(PylockTomlError::ToUrl)?;
|
||||
Ok(PylockTomlWheel {
|
||||
// Optional "when the last component of path/ url would be the same value".
|
||||
name: if url
|
||||
|
@ -232,21 +292,20 @@ impl<'lock> PylockToml {
|
|||
{
|
||||
None
|
||||
} else {
|
||||
Some(wheel.file.filename.clone())
|
||||
Some(wheel.filename.clone())
|
||||
},
|
||||
upload_time: wheel
|
||||
.file
|
||||
.upload_time_utc_ms
|
||||
.map(Timestamp::from_millisecond)
|
||||
.transpose()
|
||||
.map_err(LockErrorKind::InvalidTimestamp)?,
|
||||
.transpose()?,
|
||||
url: Some(url),
|
||||
path: None,
|
||||
size: wheel.file.size,
|
||||
hashes: Hashes::from(wheel.file.hashes),
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>, LockError>>()?,
|
||||
.collect::<Result<Vec<_>, PylockTomlError>>()?,
|
||||
)
|
||||
}
|
||||
Source::Path(..) => None,
|
||||
|
@ -360,7 +419,7 @@ impl<'lock> PylockToml {
|
|||
// Extract the `packages.sdist` field.
|
||||
let sdist = match &sdist {
|
||||
Some(SourceDist::Registry(sdist)) => {
|
||||
let url = sdist.file.url.to_url().map_err(LockErrorKind::InvalidUrl)?;
|
||||
let url = sdist.file.url.to_url().map_err(PylockTomlError::ToUrl)?;
|
||||
Some(PylockTomlSdist {
|
||||
// Optional "when the last component of path/ url would be the same value".
|
||||
name: if url
|
||||
|
@ -375,8 +434,7 @@ impl<'lock> PylockToml {
|
|||
.file
|
||||
.upload_time_utc_ms
|
||||
.map(Timestamp::from_millisecond)
|
||||
.transpose()
|
||||
.map_err(LockErrorKind::InvalidTimestamp)?,
|
||||
.transpose()?,
|
||||
url: Some(url),
|
||||
path: None,
|
||||
size,
|
||||
|
@ -485,9 +543,116 @@ impl<'lock> PylockToml {
|
|||
|
||||
Ok(doc.to_string())
|
||||
}
|
||||
|
||||
/// Convert the [`PylockToml`] to a [`Resolution`].
|
||||
pub fn to_resolution(
|
||||
self,
|
||||
install_path: &Path,
|
||||
markers: &MarkerEnvironment,
|
||||
tags: &Tags,
|
||||
) -> Result<Resolution, PylockTomlError> {
|
||||
let mut graph =
|
||||
petgraph::graph::DiGraph::with_capacity(self.packages.len(), self.packages.len());
|
||||
|
||||
// Add the root node.
|
||||
let root = graph.add_node(Node::Root);
|
||||
|
||||
for package in self.packages {
|
||||
// Omit packages that aren't relevant to the current environment.
|
||||
let install = package.marker.evaluate(markers, &[]);
|
||||
|
||||
// Search for a matching wheel.
|
||||
let dist = if let Some(best_wheel) = package.find_best_wheel(tags) {
|
||||
let hashes = HashDigests::from(best_wheel.hashes.clone());
|
||||
let built_dist = Dist::Built(BuiltDist::Registry(RegistryBuiltDist {
|
||||
wheels: vec![best_wheel.to_registry_wheel(
|
||||
install_path,
|
||||
&package.name,
|
||||
package.index.as_ref(),
|
||||
)?],
|
||||
best_wheel_index: 0,
|
||||
sdist: None,
|
||||
}));
|
||||
let dist = ResolvedDist::Installable {
|
||||
dist: Arc::new(built_dist),
|
||||
version: package.version,
|
||||
};
|
||||
Node::Dist {
|
||||
dist,
|
||||
hashes,
|
||||
install,
|
||||
}
|
||||
} else if let Some(sdist) = package.sdist.as_ref() {
|
||||
let hashes = HashDigests::from(sdist.hashes.clone());
|
||||
let sdist = Dist::Source(SourceDist::Registry(sdist.to_sdist(
|
||||
install_path,
|
||||
&package.name,
|
||||
package.version.as_ref(),
|
||||
package.index.as_ref(),
|
||||
)?));
|
||||
let dist = ResolvedDist::Installable {
|
||||
dist: Arc::new(sdist),
|
||||
version: package.version,
|
||||
};
|
||||
Node::Dist {
|
||||
dist,
|
||||
hashes,
|
||||
install,
|
||||
}
|
||||
} else if let Some(sdist) = package.directory.as_ref() {
|
||||
let hashes = HashDigests::empty();
|
||||
let sdist = Dist::Source(SourceDist::Directory(
|
||||
sdist.to_sdist(install_path, &package.name)?,
|
||||
));
|
||||
let dist = ResolvedDist::Installable {
|
||||
dist: Arc::new(sdist),
|
||||
version: package.version,
|
||||
};
|
||||
Node::Dist {
|
||||
dist,
|
||||
hashes,
|
||||
install,
|
||||
}
|
||||
} else if let Some(sdist) = package.vcs.as_ref() {
|
||||
let hashes = HashDigests::empty();
|
||||
let sdist = Dist::Source(SourceDist::Git(
|
||||
sdist.to_sdist(install_path, &package.name)?,
|
||||
));
|
||||
let dist = ResolvedDist::Installable {
|
||||
dist: Arc::new(sdist),
|
||||
version: package.version,
|
||||
};
|
||||
Node::Dist {
|
||||
dist,
|
||||
hashes,
|
||||
install,
|
||||
}
|
||||
} else if let Some(dist) = package.archive.as_ref() {
|
||||
let hashes = HashDigests::from(dist.hashes.clone());
|
||||
let dist = dist.to_dist(install_path, &package.name, package.version.as_ref())?;
|
||||
let dist = ResolvedDist::Installable {
|
||||
dist: Arc::new(dist),
|
||||
version: package.version,
|
||||
};
|
||||
Node::Dist {
|
||||
dist,
|
||||
hashes,
|
||||
install,
|
||||
}
|
||||
} else {
|
||||
return Err(PylockTomlError::MissingSource(package.name.clone()));
|
||||
};
|
||||
|
||||
let index = graph.add_node(dist);
|
||||
graph.add_edge(root, index, Edge::Prod(package.marker));
|
||||
}
|
||||
|
||||
Ok(Resolution::new(graph))
|
||||
}
|
||||
}
|
||||
|
||||
impl PylockTomlPackage {
|
||||
/// Convert the [`PylockTomlPackage`] to a TOML [`Table`].
|
||||
fn to_toml(&self) -> Result<Table, toml_edit::ser::Error> {
|
||||
let mut table = Table::new();
|
||||
table.insert("name", value(self.name.to_string()));
|
||||
|
@ -571,6 +736,338 @@ impl PylockTomlPackage {
|
|||
|
||||
Ok(table)
|
||||
}
|
||||
|
||||
/// Return the index of the best wheel for the given tags.
|
||||
fn find_best_wheel(&self, tags: &Tags) -> Option<&PylockTomlWheel> {
|
||||
type WheelPriority = (TagPriority, Option<BuildTag>);
|
||||
|
||||
let mut best: Option<(WheelPriority, &PylockTomlWheel)> = None;
|
||||
for wheel in self.wheels.iter().flatten() {
|
||||
let Ok(filename) = wheel.filename(&self.name) else {
|
||||
continue;
|
||||
};
|
||||
let TagCompatibility::Compatible(tag_priority) = filename.compatibility(tags) else {
|
||||
continue;
|
||||
};
|
||||
let build_tag = filename.build_tag().cloned();
|
||||
let wheel_priority = (tag_priority, build_tag);
|
||||
match &best {
|
||||
None => {
|
||||
best = Some((wheel_priority, wheel));
|
||||
}
|
||||
Some((best_priority, _)) => {
|
||||
if wheel_priority > *best_priority {
|
||||
best = Some((wheel_priority, wheel));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
best.map(|(_, i)| i)
|
||||
}
|
||||
}
|
||||
|
||||
impl PylockTomlWheel {
|
||||
/// Return the [`WheelFilename`] for this wheel.
|
||||
fn filename(&self, name: &PackageName) -> Result<Cow<'_, WheelFilename>, PylockTomlError> {
|
||||
if let Some(name) = self.name.as_ref() {
|
||||
Ok(Cow::Borrowed(name))
|
||||
} else if let Some(path) = self.path.as_ref() {
|
||||
let Some(filename) = path.as_ref().file_name().and_then(OsStr::to_str) else {
|
||||
return Err(PylockTomlError::PathMissingFilename(Box::<Path>::from(
|
||||
path.clone(),
|
||||
)));
|
||||
};
|
||||
let filename = WheelFilename::from_str(filename).map(Cow::Owned)?;
|
||||
Ok(filename)
|
||||
} else if let Some(url) = self.url.as_ref() {
|
||||
let Some(filename) = url.filename().ok() else {
|
||||
return Err(PylockTomlError::UrlMissingFilename(url.clone()));
|
||||
};
|
||||
let filename = WheelFilename::from_str(&filename).map(Cow::Owned)?;
|
||||
Ok(filename)
|
||||
} else {
|
||||
Err(PylockTomlError::WheelMissingPathUrl(name.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert the wheel to a [`RegistryBuiltWheel`].
|
||||
fn to_registry_wheel(
|
||||
&self,
|
||||
install_path: &Path,
|
||||
name: &PackageName,
|
||||
index: Option<&Url>,
|
||||
) -> Result<RegistryBuiltWheel, PylockTomlError> {
|
||||
let filename = self.filename(name)?.into_owned();
|
||||
|
||||
let file_url = if let Some(url) = self.url.as_ref() {
|
||||
UrlString::from(url)
|
||||
} else if let Some(path) = self.path.as_ref() {
|
||||
let path = install_path.join(path);
|
||||
let url = Url::from_file_path(path).map_err(|()| PylockTomlError::PathToUrl)?;
|
||||
UrlString::from(url)
|
||||
} else {
|
||||
return Err(PylockTomlError::WheelMissingPathUrl(name.clone()));
|
||||
};
|
||||
|
||||
let index = if let Some(index) = index {
|
||||
IndexUrl::from(VerbatimUrl::from_url(index.clone()))
|
||||
} else {
|
||||
// Including the index is only a SHOULD in PEP 751. If it's omitted, we treat the
|
||||
// URL (less the filename) as the index. This isn't correct, but it's the best we can
|
||||
// do. In practice, the only effect here should be that we cache the wheel under a hash
|
||||
// of this URL (since we cache under the hash of the index).
|
||||
let mut index = file_url.to_url().map_err(PylockTomlError::ToUrl)?;
|
||||
index.path_segments_mut().unwrap().pop();
|
||||
IndexUrl::from(VerbatimUrl::from_url(index))
|
||||
};
|
||||
|
||||
let file = Box::new(uv_distribution_types::File {
|
||||
dist_info_metadata: false,
|
||||
filename: SmallString::from(filename.to_string()),
|
||||
hashes: HashDigests::from(self.hashes.clone()),
|
||||
requires_python: None,
|
||||
size: self.size,
|
||||
upload_time_utc_ms: self.upload_time.map(Timestamp::as_millisecond),
|
||||
url: FileLocation::AbsoluteUrl(file_url),
|
||||
yanked: None,
|
||||
});
|
||||
|
||||
Ok(RegistryBuiltWheel {
|
||||
filename,
|
||||
file,
|
||||
index,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl PylockTomlDirectory {
|
||||
/// Convert the sdist to a [`DirectorySourceDist`].
|
||||
fn to_sdist(
|
||||
&self,
|
||||
install_path: &Path,
|
||||
name: &PackageName,
|
||||
) -> Result<DirectorySourceDist, PylockTomlError> {
|
||||
let path = if let Some(subdirectory) = self.subdirectory.as_ref() {
|
||||
install_path.join(&self.path).join(subdirectory)
|
||||
} else {
|
||||
install_path.join(&self.path)
|
||||
};
|
||||
let path = uv_fs::normalize_path_buf(path);
|
||||
let url =
|
||||
VerbatimUrl::from_normalized_path(&path).map_err(|_| PylockTomlError::PathToUrl)?;
|
||||
Ok(DirectorySourceDist {
|
||||
name: name.clone(),
|
||||
install_path: path.into_boxed_path(),
|
||||
editable: self.editable.unwrap_or(false),
|
||||
r#virtual: false,
|
||||
url,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl PylockTomlVcs {
|
||||
/// Convert the sdist to a [`GitSourceDist`].
|
||||
fn to_sdist(
|
||||
&self,
|
||||
install_path: &Path,
|
||||
name: &PackageName,
|
||||
) -> Result<GitSourceDist, PylockTomlError> {
|
||||
let subdirectory = self.subdirectory.clone().map(Box::<Path>::from);
|
||||
|
||||
// Reconstruct the `GitUrl` from the individual fields.
|
||||
let git_url = {
|
||||
let mut url = if let Some(url) = self.url.as_ref() {
|
||||
url.clone()
|
||||
} else if let Some(path) = self.path.as_ref() {
|
||||
Url::from_directory_path(install_path.join(path))
|
||||
.map_err(|()| PylockTomlError::PathToUrl)?
|
||||
} else {
|
||||
return Err(PylockTomlError::VcsMissingPathUrl(name.clone()));
|
||||
};
|
||||
url.set_fragment(None);
|
||||
url.set_query(None);
|
||||
|
||||
let reference = self
|
||||
.requested_revision
|
||||
.clone()
|
||||
.map(GitReference::from_rev)
|
||||
.unwrap_or_else(|| GitReference::BranchOrTagOrCommit(self.commit_id.to_string()));
|
||||
let precise = self.commit_id;
|
||||
|
||||
GitUrl::from_commit(url, reference, precise)?
|
||||
};
|
||||
|
||||
// Reconstruct the PEP 508-compatible URL from the `GitSource`.
|
||||
let url = Url::from(ParsedGitUrl {
|
||||
url: git_url.clone(),
|
||||
subdirectory: subdirectory.clone(),
|
||||
});
|
||||
|
||||
Ok(GitSourceDist {
|
||||
name: name.clone(),
|
||||
git: Box::new(git_url),
|
||||
subdirectory: self.subdirectory.clone().map(Box::<Path>::from),
|
||||
url: VerbatimUrl::from_url(url),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl PylockTomlSdist {
|
||||
/// Return the filename for this sdist.
|
||||
fn filename(&self, name: &PackageName) -> Result<Cow<'_, SmallString>, PylockTomlError> {
|
||||
if let Some(name) = self.name.as_ref() {
|
||||
Ok(Cow::Borrowed(name))
|
||||
} else if let Some(path) = self.path.as_ref() {
|
||||
let Some(filename) = path.as_ref().file_name().and_then(OsStr::to_str) else {
|
||||
return Err(PylockTomlError::PathMissingFilename(Box::<Path>::from(
|
||||
path.clone(),
|
||||
)));
|
||||
};
|
||||
Ok(Cow::Owned(SmallString::from(filename)))
|
||||
} else if let Some(url) = self.url.as_ref() {
|
||||
let Some(filename) = url.filename().ok() else {
|
||||
return Err(PylockTomlError::UrlMissingFilename(url.clone()));
|
||||
};
|
||||
Ok(Cow::Owned(SmallString::from(filename)))
|
||||
} else {
|
||||
Err(PylockTomlError::SdistMissingPathUrl(name.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert the sdist to a [`RegistrySourceDist`].
|
||||
fn to_sdist(
|
||||
&self,
|
||||
install_path: &Path,
|
||||
name: &PackageName,
|
||||
version: Option<&Version>,
|
||||
index: Option<&Url>,
|
||||
) -> Result<RegistrySourceDist, PylockTomlError> {
|
||||
let filename = self.filename(name)?.into_owned();
|
||||
let ext = SourceDistExtension::from_path(filename.as_ref())?;
|
||||
|
||||
let version = if let Some(version) = version {
|
||||
Cow::Borrowed(version)
|
||||
} else {
|
||||
let filename = SourceDistFilename::parse(&filename, ext, name)?;
|
||||
Cow::Owned(filename.version)
|
||||
};
|
||||
|
||||
let file_url = if let Some(url) = self.url.as_ref() {
|
||||
UrlString::from(url)
|
||||
} else if let Some(path) = self.path.as_ref() {
|
||||
let path = install_path.join(path);
|
||||
let url = Url::from_file_path(path).map_err(|()| PylockTomlError::PathToUrl)?;
|
||||
UrlString::from(url)
|
||||
} else {
|
||||
return Err(PylockTomlError::SdistMissingPathUrl(name.clone()));
|
||||
};
|
||||
|
||||
let index = if let Some(index) = index {
|
||||
IndexUrl::from(VerbatimUrl::from_url(index.clone()))
|
||||
} else {
|
||||
// Including the index is only a SHOULD in PEP 751. If it's omitted, we treat the
|
||||
// URL (less the filename) as the index. This isn't correct, but it's the best we can
|
||||
// do. In practice, the only effect here should be that we cache the sdist under a hash
|
||||
// of this URL (since we cache under the hash of the index).
|
||||
let mut index = file_url.to_url().map_err(PylockTomlError::ToUrl)?;
|
||||
index.path_segments_mut().unwrap().pop();
|
||||
IndexUrl::from(VerbatimUrl::from_url(index))
|
||||
};
|
||||
|
||||
let file = Box::new(uv_distribution_types::File {
|
||||
dist_info_metadata: false,
|
||||
filename,
|
||||
hashes: HashDigests::from(self.hashes.clone()),
|
||||
requires_python: None,
|
||||
size: self.size,
|
||||
upload_time_utc_ms: self.upload_time.map(Timestamp::as_millisecond),
|
||||
url: FileLocation::AbsoluteUrl(file_url),
|
||||
yanked: None,
|
||||
});
|
||||
|
||||
Ok(RegistrySourceDist {
|
||||
name: name.clone(),
|
||||
version: version.into_owned(),
|
||||
file,
|
||||
ext,
|
||||
index,
|
||||
wheels: vec![],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl PylockTomlArchive {
|
||||
fn to_dist(
|
||||
&self,
|
||||
install_path: &Path,
|
||||
name: &PackageName,
|
||||
version: Option<&Version>,
|
||||
) -> Result<Dist, PylockTomlError> {
|
||||
if let Some(url) = self.url.as_ref() {
|
||||
let filename = url
|
||||
.filename()
|
||||
.map_err(|_| PylockTomlError::UrlMissingFilename(url.clone()))?;
|
||||
|
||||
let ext = DistExtension::from_path(filename.as_ref())?;
|
||||
match ext {
|
||||
DistExtension::Wheel => {
|
||||
let filename = WheelFilename::from_str(&filename)?;
|
||||
Ok(Dist::Built(BuiltDist::DirectUrl(DirectUrlBuiltDist {
|
||||
filename,
|
||||
location: Box::new(url.clone()),
|
||||
url: VerbatimUrl::from_url(url.clone()),
|
||||
})))
|
||||
}
|
||||
DistExtension::Source(ext) => {
|
||||
Ok(Dist::Source(SourceDist::DirectUrl(DirectUrlSourceDist {
|
||||
name: name.clone(),
|
||||
location: Box::new(url.clone()),
|
||||
subdirectory: self.subdirectory.clone().map(Box::<Path>::from),
|
||||
ext,
|
||||
url: VerbatimUrl::from_url(url.clone()),
|
||||
})))
|
||||
}
|
||||
}
|
||||
} else if let Some(path) = self.path.as_ref() {
|
||||
let filename = path
|
||||
.as_ref()
|
||||
.file_name()
|
||||
.and_then(OsStr::to_str)
|
||||
.ok_or_else(|| {
|
||||
PylockTomlError::PathMissingFilename(Box::<Path>::from(path.clone()))
|
||||
})?;
|
||||
|
||||
let ext = DistExtension::from_path(filename)?;
|
||||
match ext {
|
||||
DistExtension::Wheel => {
|
||||
let filename = WheelFilename::from_str(filename)?;
|
||||
let install_path = install_path.join(path);
|
||||
let url = VerbatimUrl::from_absolute_path(&install_path)
|
||||
.map_err(|_| PylockTomlError::PathToUrl)?;
|
||||
Ok(Dist::Built(BuiltDist::Path(PathBuiltDist {
|
||||
filename,
|
||||
install_path: install_path.into_boxed_path(),
|
||||
url,
|
||||
})))
|
||||
}
|
||||
DistExtension::Source(ext) => {
|
||||
let install_path = install_path.join(path);
|
||||
let url = VerbatimUrl::from_absolute_path(&install_path)
|
||||
.map_err(|_| PylockTomlError::PathToUrl)?;
|
||||
Ok(Dist::Source(SourceDist::Path(PathSourceDist {
|
||||
name: name.clone(),
|
||||
version: version.cloned(),
|
||||
install_path: install_path.into_boxed_path(),
|
||||
ext,
|
||||
url,
|
||||
})))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Err(PylockTomlError::ArchiveMissingPathUrl(name.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a Jiff timestamp to a TOML datetime.
|
||||
|
@ -602,3 +1099,51 @@ where
|
|||
};
|
||||
serializer.serialize_some(×tamp)
|
||||
}
|
||||
|
||||
/// Convert a TOML datetime to a Jiff timestamp.
|
||||
fn timestamp_from_toml_datetime<'de, D>(deserializer: D) -> Result<Option<Timestamp>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let Some(datetime) = Option::<toml_edit::Datetime>::deserialize(deserializer)? else {
|
||||
return Ok(None);
|
||||
};
|
||||
let Some(date) = datetime.date else {
|
||||
return Err(serde::de::Error::custom("missing date"));
|
||||
};
|
||||
|
||||
let year = i16::try_from(date.year).map_err(serde::de::Error::custom)?;
|
||||
let month = i8::try_from(date.month).map_err(serde::de::Error::custom)?;
|
||||
let day = i8::try_from(date.day).map_err(serde::de::Error::custom)?;
|
||||
let date = civil::date(year, month, day);
|
||||
|
||||
// If the timezone is omitted, assume UTC.
|
||||
let tz = if let Some(offset) = datetime.offset {
|
||||
match offset {
|
||||
toml_edit::Offset::Z => TimeZone::UTC,
|
||||
toml_edit::Offset::Custom { minutes } => {
|
||||
let hours = i8::try_from(minutes / 60).map_err(serde::de::Error::custom)?;
|
||||
TimeZone::fixed(Offset::constant(hours))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
TimeZone::UTC
|
||||
};
|
||||
|
||||
// If the time is omitted, assume midnight.
|
||||
let time = if let Some(time) = datetime.time {
|
||||
let hour = i8::try_from(time.hour).map_err(serde::de::Error::custom)?;
|
||||
let minute = i8::try_from(time.minute).map_err(serde::de::Error::custom)?;
|
||||
let second = i8::try_from(time.second).map_err(serde::de::Error::custom)?;
|
||||
let nanosecond = i32::try_from(time.nanosecond).map_err(serde::de::Error::custom)?;
|
||||
Time::constant(hour, minute, second, nanosecond)
|
||||
} else {
|
||||
Time::midnight()
|
||||
};
|
||||
|
||||
let zoned = DateTime::from_parts(date, time)
|
||||
.to_zoned(tz)
|
||||
.map_err(serde::de::Error::custom)?;
|
||||
let timestamp = zoned.timestamp();
|
||||
Ok(Some(timestamp))
|
||||
}
|
||||
|
|
|
@ -2202,7 +2202,7 @@ impl Package {
|
|||
let wheels = self
|
||||
.wheels
|
||||
.iter()
|
||||
.map(|wheel| wheel.to_registry_dist(source, workspace_root))
|
||||
.map(|wheel| wheel.to_registry_wheel(source, workspace_root))
|
||||
.collect::<Result<_, LockError>>()?;
|
||||
let reg_built_dist = RegistryBuiltDist {
|
||||
wheels,
|
||||
|
@ -4183,7 +4183,7 @@ impl Wheel {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn to_registry_dist(
|
||||
pub(crate) fn to_registry_wheel(
|
||||
&self,
|
||||
source: &RegistrySource,
|
||||
root: &Path,
|
||||
|
|
|
@ -171,6 +171,7 @@ pub(crate) async fn pip_compile(
|
|||
requirements,
|
||||
constraints,
|
||||
overrides,
|
||||
pylock,
|
||||
source_trees,
|
||||
groups,
|
||||
extras: used_extras,
|
||||
|
@ -189,6 +190,13 @@ pub(crate) async fn pip_compile(
|
|||
)
|
||||
.await?;
|
||||
|
||||
// Reject `pylock.toml` files, which are valid outputs but not inputs.
|
||||
if pylock.is_some() {
|
||||
return Err(anyhow!(
|
||||
"`pylock.toml` is not a supported input format for `uv pip compile`"
|
||||
));
|
||||
}
|
||||
|
||||
let constraints = constraints
|
||||
.iter()
|
||||
.cloned()
|
||||
|
|
|
@ -32,8 +32,8 @@ use uv_python::{
|
|||
};
|
||||
use uv_requirements::{RequirementsSource, RequirementsSpecification};
|
||||
use uv_resolver::{
|
||||
DependencyMode, ExcludeNewer, FlatIndex, OptionsBuilder, PrereleaseMode, PythonRequirement,
|
||||
ResolutionMode, ResolverEnvironment,
|
||||
DependencyMode, ExcludeNewer, FlatIndex, OptionsBuilder, PrereleaseMode, PylockToml,
|
||||
PythonRequirement, ResolutionMode, ResolverEnvironment,
|
||||
};
|
||||
use uv_torch::{TorchMode, TorchStrategy};
|
||||
use uv_types::{BuildIsolation, HashStrategy};
|
||||
|
@ -111,6 +111,7 @@ pub(crate) async fn pip_install(
|
|||
requirements,
|
||||
constraints,
|
||||
overrides,
|
||||
pylock,
|
||||
source_trees,
|
||||
groups,
|
||||
index_url,
|
||||
|
@ -130,6 +131,12 @@ pub(crate) async fn pip_install(
|
|||
)
|
||||
.await?;
|
||||
|
||||
if pylock.is_some() {
|
||||
if preview.is_disabled() {
|
||||
warn_user!("The `--pylock` setting is experimental and may change without warning. Pass `--preview` to disable this warning.");
|
||||
}
|
||||
}
|
||||
|
||||
let constraints: Vec<NameRequirementSpecification> = constraints
|
||||
.iter()
|
||||
.cloned()
|
||||
|
@ -245,6 +252,7 @@ pub(crate) async fn pip_install(
|
|||
if reinstall.is_none()
|
||||
&& upgrade.is_none()
|
||||
&& source_trees.is_empty()
|
||||
&& pylock.is_none()
|
||||
&& matches!(modifications, Modifications::Sufficient)
|
||||
{
|
||||
match site_packages.satisfies_spec(&requirements, &constraints, &overrides, &marker_env)? {
|
||||
|
@ -305,9 +313,6 @@ pub(crate) async fn pip_install(
|
|||
HashStrategy::None
|
||||
};
|
||||
|
||||
// When resolving, don't take any external preferences into account.
|
||||
let preferences = Vec::default();
|
||||
|
||||
// Incorporate any index locations from the provided sources.
|
||||
let index_locations = index_locations.combine(
|
||||
extra_index_urls
|
||||
|
@ -429,51 +434,69 @@ pub(crate) async fn pip_install(
|
|||
preview,
|
||||
);
|
||||
|
||||
let options = OptionsBuilder::new()
|
||||
.resolution_mode(resolution_mode)
|
||||
.prerelease_mode(prerelease_mode)
|
||||
.dependency_mode(dependency_mode)
|
||||
.exclude_newer(exclude_newer)
|
||||
.index_strategy(index_strategy)
|
||||
.build_options(build_options.clone())
|
||||
.build();
|
||||
let (resolution, hasher) = if let Some(pylock) = pylock {
|
||||
// Read the `pylock.toml` from disk, and deserialize it from TOML.
|
||||
let install_path = std::path::absolute(&pylock)?;
|
||||
let install_path = install_path.parent().unwrap();
|
||||
let content = fs_err::tokio::read_to_string(&pylock).await?;
|
||||
let lock = toml::from_str::<PylockToml>(&content)?;
|
||||
|
||||
// Resolve the requirements.
|
||||
let resolution = match operations::resolve(
|
||||
requirements,
|
||||
constraints,
|
||||
overrides,
|
||||
source_trees,
|
||||
project,
|
||||
BTreeSet::default(),
|
||||
extras,
|
||||
&groups,
|
||||
preferences,
|
||||
site_packages.clone(),
|
||||
&hasher,
|
||||
&reinstall,
|
||||
&upgrade,
|
||||
Some(&tags),
|
||||
ResolverEnvironment::specific(marker_env.clone()),
|
||||
python_requirement,
|
||||
Conflicts::empty(),
|
||||
&client,
|
||||
&flat_index,
|
||||
state.index(),
|
||||
&build_dispatch,
|
||||
concurrency,
|
||||
options,
|
||||
Box::new(DefaultResolveLogger),
|
||||
printer,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(graph) => Resolution::from(graph),
|
||||
Err(err) => {
|
||||
return diagnostics::OperationDiagnostic::native_tls(network_settings.native_tls)
|
||||
.report(err)
|
||||
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()))
|
||||
}
|
||||
let resolution = lock.to_resolution(install_path, marker_env.markers(), &tags)?;
|
||||
let hasher = HashStrategy::from_resolution(&resolution, HashCheckingMode::Verify)?;
|
||||
|
||||
(resolution, hasher)
|
||||
} else {
|
||||
// When resolving, don't take any external preferences into account.
|
||||
let preferences = Vec::default();
|
||||
|
||||
let options = OptionsBuilder::new()
|
||||
.resolution_mode(resolution_mode)
|
||||
.prerelease_mode(prerelease_mode)
|
||||
.dependency_mode(dependency_mode)
|
||||
.exclude_newer(exclude_newer)
|
||||
.index_strategy(index_strategy)
|
||||
.build_options(build_options.clone())
|
||||
.build();
|
||||
|
||||
// Resolve the requirements.
|
||||
let resolution = match operations::resolve(
|
||||
requirements,
|
||||
constraints,
|
||||
overrides,
|
||||
source_trees,
|
||||
project,
|
||||
BTreeSet::default(),
|
||||
extras,
|
||||
&groups,
|
||||
preferences,
|
||||
site_packages.clone(),
|
||||
&hasher,
|
||||
&reinstall,
|
||||
&upgrade,
|
||||
Some(&tags),
|
||||
ResolverEnvironment::specific(marker_env.clone()),
|
||||
python_requirement,
|
||||
Conflicts::empty(),
|
||||
&client,
|
||||
&flat_index,
|
||||
state.index(),
|
||||
&build_dispatch,
|
||||
concurrency,
|
||||
options,
|
||||
Box::new(DefaultResolveLogger),
|
||||
printer,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(graph) => Resolution::from(graph),
|
||||
Err(err) => {
|
||||
return diagnostics::OperationDiagnostic::native_tls(network_settings.native_tls)
|
||||
.report(err)
|
||||
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()))
|
||||
}
|
||||
};
|
||||
|
||||
(resolution, hasher)
|
||||
};
|
||||
|
||||
// Sync the environment.
|
||||
|
@ -502,7 +525,7 @@ pub(crate) async fn pip_install(
|
|||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {}
|
||||
Ok(..) => {}
|
||||
Err(err) => {
|
||||
return diagnostics::OperationDiagnostic::native_tls(network_settings.native_tls)
|
||||
.report(err)
|
||||
|
|
|
@ -27,8 +27,8 @@ use uv_python::{
|
|||
};
|
||||
use uv_requirements::{RequirementsSource, RequirementsSpecification};
|
||||
use uv_resolver::{
|
||||
DependencyMode, ExcludeNewer, FlatIndex, OptionsBuilder, PrereleaseMode, PythonRequirement,
|
||||
ResolutionMode, ResolverEnvironment,
|
||||
DependencyMode, ExcludeNewer, FlatIndex, OptionsBuilder, PrereleaseMode, PylockToml,
|
||||
PythonRequirement, ResolutionMode, ResolverEnvironment,
|
||||
};
|
||||
use uv_torch::{TorchMode, TorchStrategy};
|
||||
use uv_types::{BuildIsolation, HashStrategy};
|
||||
|
@ -103,6 +103,7 @@ pub(crate) async fn pip_sync(
|
|||
requirements,
|
||||
constraints,
|
||||
overrides,
|
||||
pylock,
|
||||
source_trees,
|
||||
groups,
|
||||
index_url,
|
||||
|
@ -122,13 +123,20 @@ pub(crate) async fn pip_sync(
|
|||
)
|
||||
.await?;
|
||||
|
||||
if pylock.is_some() {
|
||||
if preview.is_disabled() {
|
||||
warn_user!("The `--pylock` setting is experimental and may change without warning. Pass `--preview` to disable this warning.");
|
||||
}
|
||||
}
|
||||
|
||||
// Read build constraints.
|
||||
let build_constraints =
|
||||
operations::read_constraints(build_constraints, &client_builder).await?;
|
||||
|
||||
// Validate that the requirements are non-empty.
|
||||
if !allow_empty_requirements {
|
||||
let num_requirements = requirements.len() + source_trees.len();
|
||||
let num_requirements =
|
||||
requirements.len() + source_trees.len() + usize::from(pylock.is_some());
|
||||
if num_requirements == 0 {
|
||||
writeln!(printer.stderr(), "No requirements found (hint: use `--allow-empty-requirements` to clear the environment)")?;
|
||||
return Ok(ExitStatus::Success);
|
||||
|
@ -335,9 +343,6 @@ pub(crate) async fn pip_sync(
|
|||
// Initialize any shared state.
|
||||
let state = SharedState::default();
|
||||
|
||||
// When resolving, don't take any external preferences into account.
|
||||
let preferences = Vec::default();
|
||||
|
||||
// Create a build dispatch.
|
||||
let build_dispatch = BuildDispatch::new(
|
||||
&client,
|
||||
|
@ -364,50 +369,68 @@ pub(crate) async fn pip_sync(
|
|||
// Determine the set of installed packages.
|
||||
let site_packages = SitePackages::from_environment(&environment)?;
|
||||
|
||||
let options = OptionsBuilder::new()
|
||||
.resolution_mode(resolution_mode)
|
||||
.prerelease_mode(prerelease_mode)
|
||||
.dependency_mode(dependency_mode)
|
||||
.exclude_newer(exclude_newer)
|
||||
.index_strategy(index_strategy)
|
||||
.build_options(build_options.clone())
|
||||
.build();
|
||||
let (resolution, hasher) = if let Some(pylock) = pylock {
|
||||
// Read the `pylock.toml` from disk, and deserialize it from TOML.
|
||||
let install_path = std::path::absolute(&pylock)?;
|
||||
let install_path = install_path.parent().unwrap();
|
||||
let content = fs_err::tokio::read_to_string(&pylock).await?;
|
||||
let lock = toml::from_str::<PylockToml>(&content)?;
|
||||
|
||||
let resolution = match operations::resolve(
|
||||
requirements,
|
||||
constraints,
|
||||
overrides,
|
||||
source_trees,
|
||||
project,
|
||||
BTreeSet::default(),
|
||||
&extras,
|
||||
&groups,
|
||||
preferences,
|
||||
site_packages.clone(),
|
||||
&hasher,
|
||||
&reinstall,
|
||||
&upgrade,
|
||||
Some(&tags),
|
||||
ResolverEnvironment::specific(marker_env.clone()),
|
||||
python_requirement,
|
||||
Conflicts::empty(),
|
||||
&client,
|
||||
&flat_index,
|
||||
state.index(),
|
||||
&build_dispatch,
|
||||
concurrency,
|
||||
options,
|
||||
Box::new(DefaultResolveLogger),
|
||||
printer,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(resolution) => Resolution::from(resolution),
|
||||
Err(err) => {
|
||||
return diagnostics::OperationDiagnostic::native_tls(network_settings.native_tls)
|
||||
.report(err)
|
||||
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()))
|
||||
}
|
||||
let resolution = lock.to_resolution(install_path, marker_env.markers(), &tags)?;
|
||||
let hasher = HashStrategy::from_resolution(&resolution, HashCheckingMode::Verify)?;
|
||||
|
||||
(resolution, hasher)
|
||||
} else {
|
||||
// When resolving, don't take any external preferences into account.
|
||||
let preferences = Vec::default();
|
||||
|
||||
let options = OptionsBuilder::new()
|
||||
.resolution_mode(resolution_mode)
|
||||
.prerelease_mode(prerelease_mode)
|
||||
.dependency_mode(dependency_mode)
|
||||
.exclude_newer(exclude_newer)
|
||||
.index_strategy(index_strategy)
|
||||
.build_options(build_options.clone())
|
||||
.build();
|
||||
|
||||
let resolution = match operations::resolve(
|
||||
requirements,
|
||||
constraints,
|
||||
overrides,
|
||||
source_trees,
|
||||
project,
|
||||
BTreeSet::default(),
|
||||
&extras,
|
||||
&groups,
|
||||
preferences,
|
||||
site_packages.clone(),
|
||||
&hasher,
|
||||
&reinstall,
|
||||
&upgrade,
|
||||
Some(&tags),
|
||||
ResolverEnvironment::specific(marker_env.clone()),
|
||||
python_requirement,
|
||||
Conflicts::empty(),
|
||||
&client,
|
||||
&flat_index,
|
||||
state.index(),
|
||||
&build_dispatch,
|
||||
concurrency,
|
||||
options,
|
||||
Box::new(DefaultResolveLogger),
|
||||
printer,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(resolution) => Resolution::from(resolution),
|
||||
Err(err) => {
|
||||
return diagnostics::OperationDiagnostic::native_tls(network_settings.native_tls)
|
||||
.report(err)
|
||||
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()))
|
||||
}
|
||||
};
|
||||
|
||||
(resolution, hasher)
|
||||
};
|
||||
|
||||
// Sync the environment.
|
||||
|
|
|
@ -19,7 +19,8 @@ use wiremock::{
|
|||
use crate::common::{self, decode_token};
|
||||
|
||||
use crate::common::{
|
||||
build_vendor_links_url, get_bin, uv_snapshot, venv_bin_path, venv_to_interpreter, TestContext,
|
||||
build_vendor_links_url, download_to_disk, get_bin, uv_snapshot, venv_bin_path,
|
||||
venv_to_interpreter, TestContext,
|
||||
};
|
||||
use uv_fs::Simplified;
|
||||
use uv_static::EnvVars;
|
||||
|
@ -10445,3 +10446,628 @@ fn change_layout_custom_directory() -> Result<()> {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pep_751_install_registry_wheel() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(
|
||||
r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = ["iniconfig"]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
context
|
||||
.export()
|
||||
.arg("-o")
|
||||
.arg("pylock.toml")
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
uv_snapshot!(context.filters(), context.pip_install()
|
||||
.arg("--preview")
|
||||
.arg("-r")
|
||||
.arg("pylock.toml"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Prepared 1 package in [TIME]
|
||||
Installed 1 package in [TIME]
|
||||
+ iniconfig==2.0.0
|
||||
"
|
||||
);
|
||||
|
||||
uv_snapshot!(context.filters(), context.pip_install()
|
||||
.arg("--preview")
|
||||
.arg("-r")
|
||||
.arg("pylock.toml"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Audited 1 package in [TIME]
|
||||
"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pep_751_install_registry_sdist() -> Result<()> {
|
||||
let context = TestContext::new("3.12").with_exclude_newer("2025-01-29T00:00:00Z");
|
||||
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(
|
||||
r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = ["source-distribution"]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
context
|
||||
.export()
|
||||
.arg("-o")
|
||||
.arg("pylock.toml")
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
uv_snapshot!(context.filters(), context.pip_install()
|
||||
.arg("--preview")
|
||||
.arg("-r")
|
||||
.arg("pylock.toml"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Prepared 1 package in [TIME]
|
||||
Installed 1 package in [TIME]
|
||||
+ source-distribution==0.0.3
|
||||
"
|
||||
);
|
||||
|
||||
uv_snapshot!(context.filters(), context.pip_install()
|
||||
.arg("--preview")
|
||||
.arg("-r")
|
||||
.arg("pylock.toml"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Audited 1 package in [TIME]
|
||||
"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pep_751_install_directory() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
// Create a local dependency in a subdirectory.
|
||||
let pyproject_toml = context.temp_dir.child("foo").child("pyproject.toml");
|
||||
pyproject_toml.write_str(
|
||||
r#"
|
||||
[project]
|
||||
name = "foo"
|
||||
version = "1.0.0"
|
||||
dependencies = ["anyio"]
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
"#,
|
||||
)?;
|
||||
context
|
||||
.temp_dir
|
||||
.child("foo")
|
||||
.child("src")
|
||||
.child("foo")
|
||||
.child("__init__.py")
|
||||
.touch()?;
|
||||
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(
|
||||
r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = ["foo"]
|
||||
|
||||
[tool.uv.sources]
|
||||
foo = { path = "foo" }
|
||||
"#,
|
||||
)?;
|
||||
|
||||
context
|
||||
.export()
|
||||
.arg("-o")
|
||||
.arg("pylock.toml")
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
uv_snapshot!(context.filters(), context.pip_install()
|
||||
.arg("--preview")
|
||||
.arg("-r")
|
||||
.arg("pylock.toml"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Prepared 4 packages in [TIME]
|
||||
Installed 4 packages in [TIME]
|
||||
+ anyio==4.3.0
|
||||
+ foo==1.0.0 (from file://[TEMP_DIR]/foo)
|
||||
+ idna==3.6
|
||||
+ sniffio==1.3.1
|
||||
"
|
||||
);
|
||||
|
||||
uv_snapshot!(context.filters(), context.pip_install()
|
||||
.arg("--preview")
|
||||
.arg("-r")
|
||||
.arg("pylock.toml"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Audited 4 packages in [TIME]
|
||||
"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "git")]
|
||||
fn pep_751_install_git() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(
|
||||
r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = ["uv-public-pypackage @ git+https://github.com/astral-test/uv-public-pypackage.git@0.0.1"]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
context
|
||||
.export()
|
||||
.arg("-o")
|
||||
.arg("pylock.toml")
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
uv_snapshot!(context.filters(), context.pip_install()
|
||||
.arg("--preview")
|
||||
.arg("-r")
|
||||
.arg("pylock.toml"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Prepared 1 package in [TIME]
|
||||
Installed 1 package in [TIME]
|
||||
+ uv-public-pypackage==0.1.0 (from git+https://github.com/astral-test/uv-public-pypackage.git@0dacfd662c64cb4ceb16e6cf65a157a8b715b979)
|
||||
"
|
||||
);
|
||||
|
||||
uv_snapshot!(context.filters(), context.pip_install()
|
||||
.arg("--preview")
|
||||
.arg("-r")
|
||||
.arg("pylock.toml"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Audited 1 package in [TIME]
|
||||
"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pep_751_install_url_wheel() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(
|
||||
r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = ["anyio @ https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl"]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
context
|
||||
.export()
|
||||
.arg("-o")
|
||||
.arg("pylock.toml")
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
uv_snapshot!(context.filters(), context.pip_install()
|
||||
.arg("--preview")
|
||||
.arg("-r")
|
||||
.arg("pylock.toml"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Prepared 2 packages in [TIME]
|
||||
Installed 3 packages in [TIME]
|
||||
+ anyio==4.3.0 (from https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl)
|
||||
+ idna==3.6
|
||||
+ sniffio==1.3.1
|
||||
"
|
||||
);
|
||||
|
||||
uv_snapshot!(context.filters(), context.pip_install()
|
||||
.arg("--preview")
|
||||
.arg("-r")
|
||||
.arg("pylock.toml"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Audited 3 packages in [TIME]
|
||||
"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pep_751_install_url_sdist() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(
|
||||
r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = ["anyio @ https://files.pythonhosted.org/packages/db/4d/3970183622f0330d3c23d9b8a5f52e365e50381fd484d08e3285104333d3/anyio-4.3.0.tar.gz"]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
context
|
||||
.export()
|
||||
.arg("-o")
|
||||
.arg("pylock.toml")
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
uv_snapshot!(context.filters(), context.pip_install()
|
||||
.arg("--preview")
|
||||
.arg("-r")
|
||||
.arg("pylock.toml"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Prepared 3 packages in [TIME]
|
||||
Installed 3 packages in [TIME]
|
||||
+ anyio==4.3.0 (from https://files.pythonhosted.org/packages/db/4d/3970183622f0330d3c23d9b8a5f52e365e50381fd484d08e3285104333d3/anyio-4.3.0.tar.gz)
|
||||
+ idna==3.6
|
||||
+ sniffio==1.3.1
|
||||
"
|
||||
);
|
||||
|
||||
uv_snapshot!(context.filters(), context.pip_install()
|
||||
.arg("--preview")
|
||||
.arg("-r")
|
||||
.arg("pylock.toml"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Audited 3 packages in [TIME]
|
||||
"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pep_751_install_path_wheel() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
// Download the source.
|
||||
let archive = context.temp_dir.child("iniconfig-2.0.0-py3-none-any.whl");
|
||||
download_to_disk(
|
||||
"https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl",
|
||||
&archive,
|
||||
);
|
||||
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(
|
||||
r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = ["iniconfig"]
|
||||
|
||||
[tool.uv.sources]
|
||||
iniconfig = { path = "iniconfig-2.0.0-py3-none-any.whl" }
|
||||
"#,
|
||||
)?;
|
||||
|
||||
context
|
||||
.export()
|
||||
.arg("-o")
|
||||
.arg("pylock.toml")
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
let lock = context.read("pylock.toml");
|
||||
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
insta::assert_snapshot!(
|
||||
lock, @r##"
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv export --cache-dir [CACHE_DIR] -o pylock.toml
|
||||
lock-version = "1.0"
|
||||
created-by = "uv"
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[[packages]]
|
||||
name = "iniconfig"
|
||||
version = "2.0.0"
|
||||
archive = { path = "iniconfig-2.0.0-py3-none-any.whl", hashes = { sha256 = "b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" } }
|
||||
"##
|
||||
);
|
||||
});
|
||||
|
||||
uv_snapshot!(context.filters(), context.pip_install()
|
||||
.arg("--preview")
|
||||
.arg("-r")
|
||||
.arg("pylock.toml"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Installed 1 package in [TIME]
|
||||
+ iniconfig==2.0.0 (from file://[TEMP_DIR]/iniconfig-2.0.0-py3-none-any.whl)
|
||||
"
|
||||
);
|
||||
|
||||
uv_snapshot!(context.filters(), context.pip_install()
|
||||
.arg("--preview")
|
||||
.arg("-r")
|
||||
.arg("pylock.toml"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Audited 1 package in [TIME]
|
||||
"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pep_751_install_path_sdist() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
// Download the source.
|
||||
let archive = context.temp_dir.child("iniconfig-2.0.0.tar.gz");
|
||||
download_to_disk(
|
||||
"https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz",
|
||||
&archive,
|
||||
);
|
||||
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(
|
||||
r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = ["iniconfig"]
|
||||
|
||||
[tool.uv.sources]
|
||||
iniconfig = { path = "iniconfig-2.0.0.tar.gz" }
|
||||
"#,
|
||||
)?;
|
||||
|
||||
context
|
||||
.export()
|
||||
.arg("-o")
|
||||
.arg("pylock.toml")
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
uv_snapshot!(context.filters(), context.pip_install()
|
||||
.arg("--preview")
|
||||
.arg("-r")
|
||||
.arg("pylock.toml"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Prepared 1 package in [TIME]
|
||||
Installed 1 package in [TIME]
|
||||
+ iniconfig==2.0.0 (from file://[TEMP_DIR]/iniconfig-2.0.0.tar.gz)
|
||||
"
|
||||
);
|
||||
|
||||
uv_snapshot!(context.filters(), context.pip_install()
|
||||
.arg("--preview")
|
||||
.arg("-r")
|
||||
.arg("pylock.toml"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Audited 1 package in [TIME]
|
||||
"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pep_751_hash_mismatch() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
// Download the source.
|
||||
let archive = context.temp_dir.child("iniconfig-2.0.0-py3-none-any.whl");
|
||||
download_to_disk(
|
||||
"https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl",
|
||||
&archive,
|
||||
);
|
||||
|
||||
let pylock_toml = context.temp_dir.child("pylock.toml");
|
||||
pylock_toml.write_str(r#"
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv export --cache-dir [CACHE_DIR] -o pylock.toml
|
||||
lock-version = "1.0"
|
||||
created-by = "uv"
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[[packages]]
|
||||
name = "iniconfig"
|
||||
version = "2.0.0"
|
||||
archive = { path = "iniconfig-2.0.0-py3-none-any.whl", hashes = { sha256 = "c5185871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" } }
|
||||
"#)?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.pip_install()
|
||||
.arg("--preview")
|
||||
.arg("-r")
|
||||
.arg("pylock.toml"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
× Failed to read `iniconfig @ file://[TEMP_DIR]/iniconfig-2.0.0-py3-none-any.whl`
|
||||
╰─▶ Hash mismatch for `iniconfig @ file://[TEMP_DIR]/iniconfig-2.0.0-py3-none-any.whl`
|
||||
|
||||
Expected:
|
||||
sha256:c5185871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374
|
||||
|
||||
Computed:
|
||||
sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374
|
||||
"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pep_751_mix() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(
|
||||
r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = ["iniconfig"]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
context
|
||||
.export()
|
||||
.arg("-o")
|
||||
.arg("pylock.toml")
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
context
|
||||
.export()
|
||||
.arg("-o")
|
||||
.arg("pylock.dev.toml")
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
context.temp_dir.child("requirements.txt").touch()?;
|
||||
context.temp_dir.child("constraints.txt").touch()?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.pip_install()
|
||||
.arg("--preview")
|
||||
.arg("-r")
|
||||
.arg("pylock.toml")
|
||||
.arg("-r")
|
||||
.arg("pylock.dev.toml"), @r"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: Multiple `pylock.toml` files specified: `pylock.toml` vs. `pylock.dev.toml`
|
||||
"
|
||||
);
|
||||
|
||||
uv_snapshot!(context.filters(), context.pip_install()
|
||||
.arg("--preview")
|
||||
.arg("-r")
|
||||
.arg("pylock.toml")
|
||||
.arg("-r")
|
||||
.arg("requirements.txt"), @r"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: Cannot specify additional requirements alongside a `pylock.toml` file
|
||||
"
|
||||
);
|
||||
|
||||
uv_snapshot!(context.filters(), context.pip_install()
|
||||
.arg("--preview")
|
||||
.arg("-r")
|
||||
.arg("pylock.toml")
|
||||
.arg("-c")
|
||||
.arg("constraints.txt"), @r"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: Cannot specify additional requirements with a `pylock.toml` file
|
||||
"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -5733,3 +5733,92 @@ fn semicolon_no_space() -> Result<()> {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pep_751() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(
|
||||
r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = ["anyio"]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
context
|
||||
.export()
|
||||
.arg("-o")
|
||||
.arg("pylock.toml")
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
uv_snapshot!(context.filters(), context.pip_sync()
|
||||
.arg("--preview")
|
||||
.arg("pylock.toml"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Prepared 3 packages in [TIME]
|
||||
Installed 3 packages in [TIME]
|
||||
+ anyio==4.3.0
|
||||
+ idna==3.6
|
||||
+ sniffio==1.3.1
|
||||
"
|
||||
);
|
||||
|
||||
uv_snapshot!(context.filters(), context.pip_sync()
|
||||
.arg("--preview")
|
||||
.arg("pylock.toml"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Audited 3 packages in [TIME]
|
||||
"
|
||||
);
|
||||
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(
|
||||
r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = ["iniconfig"]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
context
|
||||
.export()
|
||||
.arg("-o")
|
||||
.arg("pylock.toml")
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
uv_snapshot!(context.filters(), context.pip_sync()
|
||||
.arg("--preview")
|
||||
.arg("pylock.toml"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Prepared 1 package in [TIME]
|
||||
Uninstalled 3 packages in [TIME]
|
||||
Installed 1 package in [TIME]
|
||||
- anyio==4.3.0
|
||||
- idna==3.6
|
||||
+ iniconfig==2.0.0
|
||||
- sniffio==1.3.1
|
||||
"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue