mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-03 02:22:19 +00:00
Support tool.uv
in PEP 723 scripts (#5990)
## Summary This includes both _settings_ and _sources. Closes https://github.com/astral-sh/uv/issues/5855.
This commit is contained in:
parent
19ac9af167
commit
f10c28225c
14 changed files with 625 additions and 299 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -5110,6 +5110,8 @@ dependencies = [
|
|||
"serde",
|
||||
"thiserror",
|
||||
"toml",
|
||||
"uv-settings",
|
||||
"uv-workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
@ -2,7 +2,7 @@ pub use distribution_database::{DistributionDatabase, HttpArchivePointer, LocalA
|
|||
pub use download::LocalWheel;
|
||||
pub use error::Error;
|
||||
pub use index::{BuiltWheelIndex, RegistryWheelIndex};
|
||||
pub use metadata::{ArchiveMetadata, Metadata, RequiresDist};
|
||||
pub use metadata::{ArchiveMetadata, LoweredRequirement, Metadata, RequiresDist};
|
||||
pub use reporter::Reporter;
|
||||
|
||||
mod archive;
|
||||
|
|
|
@ -17,6 +17,218 @@ use uv_warnings::warn_user_once;
|
|||
use uv_workspace::pyproject::Source;
|
||||
use uv_workspace::Workspace;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LoweredRequirement(Requirement);
|
||||
|
||||
impl LoweredRequirement {
|
||||
/// Combine `project.dependencies` or `project.optional-dependencies` with `tool.uv.sources`.
|
||||
pub(crate) fn from_requirement(
|
||||
requirement: pep508_rs::Requirement<VerbatimParsedUrl>,
|
||||
project_name: &PackageName,
|
||||
project_dir: &Path,
|
||||
project_sources: &BTreeMap<PackageName, Source>,
|
||||
workspace: &Workspace,
|
||||
preview: PreviewMode,
|
||||
) -> Result<Self, LoweringError> {
|
||||
let source = project_sources
|
||||
.get(&requirement.name)
|
||||
.or(workspace.sources().get(&requirement.name))
|
||||
.cloned();
|
||||
|
||||
let workspace_package_declared =
|
||||
// We require that when you use a package that's part of the workspace, ...
|
||||
!workspace.packages().contains_key(&requirement.name)
|
||||
// ... it must be declared as a workspace dependency (`workspace = true`), ...
|
||||
|| matches!(
|
||||
source,
|
||||
Some(Source::Workspace {
|
||||
// By using toml, we technically support `workspace = false`.
|
||||
workspace: true
|
||||
})
|
||||
)
|
||||
// ... except for recursive self-inclusion (extras that activate other extras), e.g.
|
||||
// `framework[machine_learning]` depends on `framework[cuda]`.
|
||||
|| &requirement.name == project_name;
|
||||
if !workspace_package_declared {
|
||||
return Err(LoweringError::UndeclaredWorkspacePackage);
|
||||
}
|
||||
|
||||
let Some(source) = source else {
|
||||
let has_sources = !project_sources.is_empty() || !workspace.sources().is_empty();
|
||||
// Support recursive editable inclusions.
|
||||
if has_sources
|
||||
&& requirement.version_or_url.is_none()
|
||||
&& &requirement.name != project_name
|
||||
{
|
||||
warn_user_once!(
|
||||
"Missing version constraint (e.g., a lower bound) for `{}`",
|
||||
requirement.name
|
||||
);
|
||||
}
|
||||
return Ok(Self(Requirement::from(requirement)));
|
||||
};
|
||||
|
||||
if preview.is_disabled() {
|
||||
warn_user_once!("`uv.sources` is experimental and may change without warning");
|
||||
}
|
||||
|
||||
let source = match source {
|
||||
Source::Git {
|
||||
git,
|
||||
subdirectory,
|
||||
rev,
|
||||
tag,
|
||||
branch,
|
||||
} => {
|
||||
if matches!(requirement.version_or_url, Some(VersionOrUrl::Url(_))) {
|
||||
return Err(LoweringError::ConflictingUrls);
|
||||
}
|
||||
git_source(&git, subdirectory, rev, tag, branch)?
|
||||
}
|
||||
Source::Url { url, subdirectory } => {
|
||||
if matches!(requirement.version_or_url, Some(VersionOrUrl::Url(_))) {
|
||||
return Err(LoweringError::ConflictingUrls);
|
||||
}
|
||||
url_source(url, subdirectory)?
|
||||
}
|
||||
Source::Path { path, editable } => {
|
||||
if matches!(requirement.version_or_url, Some(VersionOrUrl::Url(_))) {
|
||||
return Err(LoweringError::ConflictingUrls);
|
||||
}
|
||||
path_source(
|
||||
path,
|
||||
project_dir,
|
||||
workspace.install_path(),
|
||||
editable.unwrap_or(false),
|
||||
)?
|
||||
}
|
||||
Source::Registry { index } => registry_source(&requirement, index)?,
|
||||
Source::Workspace {
|
||||
workspace: is_workspace,
|
||||
} => {
|
||||
if !is_workspace {
|
||||
return Err(LoweringError::WorkspaceFalse);
|
||||
}
|
||||
if matches!(requirement.version_or_url, Some(VersionOrUrl::Url(_))) {
|
||||
return Err(LoweringError::ConflictingUrls);
|
||||
}
|
||||
let member = workspace
|
||||
.packages()
|
||||
.get(&requirement.name)
|
||||
.ok_or(LoweringError::UndeclaredWorkspacePackage)?
|
||||
.clone();
|
||||
|
||||
// Say we have:
|
||||
// ```
|
||||
// root
|
||||
// ├── main_workspace <- We want to the path from here ...
|
||||
// │ ├── pyproject.toml
|
||||
// │ └── uv.lock
|
||||
// └──current_workspace
|
||||
// └── packages
|
||||
// └── current_package <- ... to here.
|
||||
// └── pyproject.toml
|
||||
// ```
|
||||
// The path we need in the lockfile: `../current_workspace/packages/current_project`
|
||||
// member root: `/root/current_workspace/packages/current_project`
|
||||
// workspace install root: `/root/current_workspace`
|
||||
// relative to workspace: `packages/current_project`
|
||||
// workspace lock root: `../current_workspace`
|
||||
// relative to main workspace: `../current_workspace/packages/current_project`
|
||||
let relative_to_workspace = relative_to(member.root(), workspace.install_path())
|
||||
.map_err(LoweringError::RelativeTo)?;
|
||||
let relative_to_main_workspace = workspace.lock_path().join(relative_to_workspace);
|
||||
let url = VerbatimUrl::parse_absolute_path(member.root())?
|
||||
.with_given(relative_to_main_workspace.to_string_lossy());
|
||||
RequirementSource::Directory {
|
||||
install_path: member.root().clone(),
|
||||
lock_path: relative_to_main_workspace,
|
||||
url,
|
||||
editable: true,
|
||||
}
|
||||
}
|
||||
Source::CatchAll { .. } => {
|
||||
// Emit a dedicated error message, which is an improvement over Serde's default error.
|
||||
return Err(LoweringError::InvalidEntry);
|
||||
}
|
||||
};
|
||||
Ok(Self(Requirement {
|
||||
name: requirement.name,
|
||||
extras: requirement.extras,
|
||||
marker: requirement.marker,
|
||||
source,
|
||||
origin: requirement.origin,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Lower a [`pep508_rs::Requirement`] in a non-workspace setting (for example, in a PEP 723
|
||||
/// script, which runs in an isolated context).
|
||||
pub fn from_non_workspace_requirement(
|
||||
requirement: pep508_rs::Requirement<VerbatimParsedUrl>,
|
||||
dir: &Path,
|
||||
sources: &BTreeMap<PackageName, Source>,
|
||||
preview: PreviewMode,
|
||||
) -> Result<Self, LoweringError> {
|
||||
let source = sources.get(&requirement.name).cloned();
|
||||
|
||||
let Some(source) = source else {
|
||||
return Ok(Self(Requirement::from(requirement)));
|
||||
};
|
||||
|
||||
if preview.is_disabled() {
|
||||
warn_user_once!("`uv.sources` is experimental and may change without warning");
|
||||
}
|
||||
|
||||
let source = match source {
|
||||
Source::Git {
|
||||
git,
|
||||
subdirectory,
|
||||
rev,
|
||||
tag,
|
||||
branch,
|
||||
} => {
|
||||
if matches!(requirement.version_or_url, Some(VersionOrUrl::Url(_))) {
|
||||
return Err(LoweringError::ConflictingUrls);
|
||||
}
|
||||
git_source(&git, subdirectory, rev, tag, branch)?
|
||||
}
|
||||
Source::Url { url, subdirectory } => {
|
||||
if matches!(requirement.version_or_url, Some(VersionOrUrl::Url(_))) {
|
||||
return Err(LoweringError::ConflictingUrls);
|
||||
}
|
||||
url_source(url, subdirectory)?
|
||||
}
|
||||
Source::Path { path, editable } => {
|
||||
if matches!(requirement.version_or_url, Some(VersionOrUrl::Url(_))) {
|
||||
return Err(LoweringError::ConflictingUrls);
|
||||
}
|
||||
path_source(path, dir, dir, editable.unwrap_or(false))?
|
||||
}
|
||||
Source::Registry { index } => registry_source(&requirement, index)?,
|
||||
Source::Workspace { .. } => {
|
||||
return Err(LoweringError::WorkspaceMember);
|
||||
}
|
||||
Source::CatchAll { .. } => {
|
||||
// Emit a dedicated error message, which is an improvement over Serde's default
|
||||
// error.
|
||||
return Err(LoweringError::InvalidEntry);
|
||||
}
|
||||
};
|
||||
Ok(Self(Requirement {
|
||||
name: requirement.name,
|
||||
extras: requirement.extras,
|
||||
marker: requirement.marker,
|
||||
source,
|
||||
origin: requirement.origin,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Convert back into a [`Requirement`].
|
||||
pub fn into_inner(self) -> Requirement {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// An error parsing and merging `tool.uv.sources` with
|
||||
/// `project.{dependencies,optional-dependencies}`.
|
||||
#[derive(Debug, Error)]
|
||||
|
@ -27,6 +239,8 @@ pub enum LoweringError {
|
|||
MoreThanOneGitRef,
|
||||
#[error("Unable to combine options in `tool.uv.sources`")]
|
||||
InvalidEntry,
|
||||
#[error("Workspace members are not allowed in non-workspace contexts")]
|
||||
WorkspaceMember,
|
||||
#[error(transparent)]
|
||||
InvalidUrl(#[from] url::ParseError),
|
||||
#[error(transparent)]
|
||||
|
@ -47,211 +261,95 @@ pub enum LoweringError {
|
|||
RelativeTo(io::Error),
|
||||
}
|
||||
|
||||
/// Combine `project.dependencies` or `project.optional-dependencies` with `tool.uv.sources`.
|
||||
pub(crate) fn lower_requirement(
|
||||
requirement: pep508_rs::Requirement<VerbatimParsedUrl>,
|
||||
project_name: &PackageName,
|
||||
project_dir: &Path,
|
||||
project_sources: &BTreeMap<PackageName, Source>,
|
||||
workspace: &Workspace,
|
||||
preview: PreviewMode,
|
||||
) -> Result<Requirement, LoweringError> {
|
||||
let source = project_sources
|
||||
.get(&requirement.name)
|
||||
.or(workspace.sources().get(&requirement.name))
|
||||
.cloned();
|
||||
/// Convert a Git source into a [`RequirementSource`].
|
||||
fn git_source(
|
||||
git: &Url,
|
||||
subdirectory: Option<String>,
|
||||
rev: Option<String>,
|
||||
tag: Option<String>,
|
||||
branch: Option<String>,
|
||||
) -> Result<RequirementSource, LoweringError> {
|
||||
let reference = match (rev, tag, branch) {
|
||||
(None, None, None) => GitReference::DefaultBranch,
|
||||
(Some(rev), None, None) => {
|
||||
if rev.starts_with("refs/") {
|
||||
GitReference::NamedRef(rev.clone())
|
||||
} else if rev.len() == 40 {
|
||||
GitReference::FullCommit(rev.clone())
|
||||
} else {
|
||||
GitReference::ShortCommit(rev.clone())
|
||||
}
|
||||
}
|
||||
(None, Some(tag), None) => GitReference::Tag(tag),
|
||||
(None, None, Some(branch)) => GitReference::Branch(branch),
|
||||
_ => return Err(LoweringError::MoreThanOneGitRef),
|
||||
};
|
||||
|
||||
let workspace_package_declared =
|
||||
// We require that when you use a package that's part of the workspace, ...
|
||||
!workspace.packages().contains_key(&requirement.name)
|
||||
// ... it must be declared as a workspace dependency (`workspace = true`), ...
|
||||
|| matches!(
|
||||
source,
|
||||
Some(Source::Workspace {
|
||||
// By using toml, we technically support `workspace = false`.
|
||||
workspace: true,
|
||||
..
|
||||
})
|
||||
)
|
||||
// ... except for recursive self-inclusion (extras that activate other extras), e.g.
|
||||
// `framework[machine_learning]` depends on `framework[cuda]`.
|
||||
|| &requirement.name == project_name;
|
||||
if !workspace_package_declared {
|
||||
return Err(LoweringError::UndeclaredWorkspacePackage);
|
||||
// Create a PEP 508-compatible URL.
|
||||
let mut url = Url::parse(&format!("git+{git}"))?;
|
||||
if let Some(rev) = reference.as_str() {
|
||||
url.set_path(&format!("{}@{}", url.path(), rev));
|
||||
}
|
||||
if let Some(subdirectory) = &subdirectory {
|
||||
url.set_fragment(Some(&format!("subdirectory={subdirectory}")));
|
||||
}
|
||||
let url = VerbatimUrl::from_url(url);
|
||||
|
||||
let repository = git.clone();
|
||||
|
||||
Ok(RequirementSource::Git {
|
||||
url,
|
||||
repository,
|
||||
reference,
|
||||
precise: None,
|
||||
subdirectory: subdirectory.map(PathBuf::from),
|
||||
})
|
||||
}
|
||||
|
||||
/// Convert a URL source into a [`RequirementSource`].
|
||||
fn url_source(url: Url, subdirectory: Option<String>) -> Result<RequirementSource, LoweringError> {
|
||||
let mut verbatim_url = url.clone();
|
||||
if verbatim_url.fragment().is_some() {
|
||||
return Err(LoweringError::ForbiddenFragment(url));
|
||||
}
|
||||
if let Some(subdirectory) = &subdirectory {
|
||||
verbatim_url.set_fragment(Some(subdirectory));
|
||||
}
|
||||
|
||||
let Some(source) = source else {
|
||||
let has_sources = !project_sources.is_empty() || !workspace.sources().is_empty();
|
||||
// Support recursive editable inclusions.
|
||||
if has_sources && requirement.version_or_url.is_none() && &requirement.name != project_name
|
||||
{
|
||||
let ext = DistExtension::from_path(url.path())
|
||||
.map_err(|err| ParsedUrlError::MissingExtensionUrl(url.to_string(), err))?;
|
||||
|
||||
let verbatim_url = VerbatimUrl::from_url(verbatim_url);
|
||||
Ok(RequirementSource::Url {
|
||||
location: url,
|
||||
subdirectory: subdirectory.map(PathBuf::from),
|
||||
ext,
|
||||
url: verbatim_url,
|
||||
})
|
||||
}
|
||||
|
||||
/// Convert a registry source into a [`RequirementSource`].
|
||||
fn registry_source(
|
||||
requirement: &pep508_rs::Requirement<VerbatimParsedUrl>,
|
||||
index: String,
|
||||
) -> Result<RequirementSource, LoweringError> {
|
||||
match &requirement.version_or_url {
|
||||
None => {
|
||||
warn_user_once!(
|
||||
"Missing version constraint (e.g., a lower bound) for `{}`",
|
||||
requirement.name
|
||||
);
|
||||
}
|
||||
return Ok(Requirement::from(requirement));
|
||||
};
|
||||
|
||||
if preview.is_disabled() {
|
||||
warn_user_once!("`uv.sources` is experimental and may change without warning");
|
||||
}
|
||||
|
||||
let source = match source {
|
||||
Source::Git {
|
||||
git,
|
||||
subdirectory,
|
||||
rev,
|
||||
tag,
|
||||
branch,
|
||||
} => {
|
||||
if matches!(requirement.version_or_url, Some(VersionOrUrl::Url(_))) {
|
||||
return Err(LoweringError::ConflictingUrls);
|
||||
}
|
||||
let reference = match (rev, tag, branch) {
|
||||
(None, None, None) => GitReference::DefaultBranch,
|
||||
(Some(rev), None, None) => {
|
||||
if rev.starts_with("refs/") {
|
||||
GitReference::NamedRef(rev.clone())
|
||||
} else if rev.len() == 40 {
|
||||
GitReference::FullCommit(rev.clone())
|
||||
} else {
|
||||
GitReference::ShortCommit(rev.clone())
|
||||
}
|
||||
}
|
||||
(None, Some(tag), None) => GitReference::Tag(tag),
|
||||
(None, None, Some(branch)) => GitReference::Branch(branch),
|
||||
_ => return Err(LoweringError::MoreThanOneGitRef),
|
||||
};
|
||||
|
||||
// Create a PEP 508-compatible URL.
|
||||
let mut url = Url::parse(&format!("git+{git}"))?;
|
||||
if let Some(rev) = reference.as_str() {
|
||||
url.set_path(&format!("{}@{}", url.path(), rev));
|
||||
}
|
||||
if let Some(subdirectory) = &subdirectory {
|
||||
url.set_fragment(Some(&format!("subdirectory={subdirectory}")));
|
||||
}
|
||||
let url = VerbatimUrl::from_url(url);
|
||||
|
||||
let repository = git.clone();
|
||||
|
||||
RequirementSource::Git {
|
||||
url,
|
||||
repository,
|
||||
reference,
|
||||
precise: None,
|
||||
subdirectory: subdirectory.map(PathBuf::from),
|
||||
}
|
||||
}
|
||||
Source::Url { url, subdirectory } => {
|
||||
if matches!(requirement.version_or_url, Some(VersionOrUrl::Url(_))) {
|
||||
return Err(LoweringError::ConflictingUrls);
|
||||
}
|
||||
|
||||
let mut verbatim_url = url.clone();
|
||||
if verbatim_url.fragment().is_some() {
|
||||
return Err(LoweringError::ForbiddenFragment(url));
|
||||
}
|
||||
if let Some(subdirectory) = &subdirectory {
|
||||
verbatim_url.set_fragment(Some(subdirectory));
|
||||
}
|
||||
|
||||
let ext = DistExtension::from_path(url.path())
|
||||
.map_err(|err| ParsedUrlError::MissingExtensionUrl(url.to_string(), err))?;
|
||||
|
||||
let verbatim_url = VerbatimUrl::from_url(verbatim_url);
|
||||
RequirementSource::Url {
|
||||
location: url,
|
||||
subdirectory: subdirectory.map(PathBuf::from),
|
||||
ext,
|
||||
url: verbatim_url,
|
||||
}
|
||||
}
|
||||
Source::Path { path, editable } => {
|
||||
if matches!(requirement.version_or_url, Some(VersionOrUrl::Url(_))) {
|
||||
return Err(LoweringError::ConflictingUrls);
|
||||
}
|
||||
path_source(
|
||||
path,
|
||||
project_dir,
|
||||
workspace.install_path(),
|
||||
editable.unwrap_or(false),
|
||||
)?
|
||||
}
|
||||
Source::Registry { index } => match requirement.version_or_url {
|
||||
None => {
|
||||
warn_user_once!(
|
||||
"Missing version constraint (e.g., a lower bound) for `{}`",
|
||||
requirement.name
|
||||
);
|
||||
RequirementSource::Registry {
|
||||
specifier: VersionSpecifiers::empty(),
|
||||
index: Some(index),
|
||||
}
|
||||
}
|
||||
Some(VersionOrUrl::VersionSpecifier(version)) => RequirementSource::Registry {
|
||||
specifier: version,
|
||||
Ok(RequirementSource::Registry {
|
||||
specifier: VersionSpecifiers::empty(),
|
||||
index: Some(index),
|
||||
},
|
||||
Some(VersionOrUrl::Url(_)) => return Err(LoweringError::ConflictingUrls),
|
||||
},
|
||||
Source::Workspace {
|
||||
workspace: is_workspace,
|
||||
} => {
|
||||
if !is_workspace {
|
||||
return Err(LoweringError::WorkspaceFalse);
|
||||
}
|
||||
if matches!(requirement.version_or_url, Some(VersionOrUrl::Url(_))) {
|
||||
return Err(LoweringError::ConflictingUrls);
|
||||
}
|
||||
let member = workspace
|
||||
.packages()
|
||||
.get(&requirement.name)
|
||||
.ok_or(LoweringError::UndeclaredWorkspacePackage)?
|
||||
.clone();
|
||||
|
||||
// Say we have:
|
||||
// ```
|
||||
// root
|
||||
// ├── main_workspace <- We want to the path from here ...
|
||||
// │ ├── pyproject.toml
|
||||
// │ └── uv.lock
|
||||
// └──current_workspace
|
||||
// └── packages
|
||||
// └── current_package <- ... to here.
|
||||
// └── pyproject.toml
|
||||
// ```
|
||||
// The path we need in the lockfile: `../current_workspace/packages/current_project`
|
||||
// member root: `/root/current_workspace/packages/current_project`
|
||||
// workspace install root: `/root/current_workspace`
|
||||
// relative to workspace: `packages/current_project`
|
||||
// workspace lock root: `../current_workspace`
|
||||
// relative to main workspace: `../current_workspace/packages/current_project`
|
||||
let relative_to_workspace = relative_to(member.root(), workspace.install_path())
|
||||
.map_err(LoweringError::RelativeTo)?;
|
||||
let relative_to_main_workspace = workspace.lock_path().join(relative_to_workspace);
|
||||
let url = VerbatimUrl::parse_absolute_path(member.root())?
|
||||
.with_given(relative_to_main_workspace.to_string_lossy());
|
||||
RequirementSource::Directory {
|
||||
install_path: member.root().clone(),
|
||||
lock_path: relative_to_main_workspace,
|
||||
url,
|
||||
editable: true,
|
||||
}
|
||||
})
|
||||
}
|
||||
Source::CatchAll { .. } => {
|
||||
// Emit a dedicated error message, which is an improvement over Serde's default error.
|
||||
return Err(LoweringError::InvalidEntry);
|
||||
}
|
||||
};
|
||||
Ok(Requirement {
|
||||
name: requirement.name,
|
||||
extras: requirement.extras,
|
||||
marker: requirement.marker,
|
||||
source,
|
||||
origin: requirement.origin,
|
||||
})
|
||||
Some(VersionOrUrl::VersionSpecifier(version)) => Ok(RequirementSource::Registry {
|
||||
specifier: version.clone(),
|
||||
index: Some(index),
|
||||
}),
|
||||
Some(VersionOrUrl::Url(_)) => Err(LoweringError::ConflictingUrls),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a path string to a file or directory source.
|
||||
|
|
|
@ -3,14 +3,16 @@ use std::path::Path;
|
|||
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::metadata::lowering::LoweringError;
|
||||
pub use crate::metadata::requires_dist::RequiresDist;
|
||||
use pep440_rs::{Version, VersionSpecifiers};
|
||||
use pypi_types::{HashDigest, Metadata23};
|
||||
use uv_configuration::{PreviewMode, SourceStrategy};
|
||||
use uv_normalize::{ExtraName, GroupName, PackageName};
|
||||
use uv_workspace::WorkspaceError;
|
||||
|
||||
pub use crate::metadata::lowering::LoweredRequirement;
|
||||
use crate::metadata::lowering::LoweringError;
|
||||
pub use crate::metadata::requires_dist::RequiresDist;
|
||||
|
||||
mod lowering;
|
||||
mod requires_dist;
|
||||
|
||||
|
|
|
@ -5,8 +5,7 @@ use uv_configuration::{PreviewMode, SourceStrategy};
|
|||
use uv_normalize::{ExtraName, GroupName, PackageName, DEV_DEPENDENCIES};
|
||||
use uv_workspace::{DiscoveryOptions, ProjectWorkspace};
|
||||
|
||||
use crate::metadata::lowering::lower_requirement;
|
||||
use crate::metadata::MetadataError;
|
||||
use crate::metadata::{LoweredRequirement, MetadataError};
|
||||
use crate::Metadata;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
@ -91,7 +90,7 @@ impl RequiresDist {
|
|||
.cloned()
|
||||
.map(|requirement| {
|
||||
let requirement_name = requirement.name.clone();
|
||||
lower_requirement(
|
||||
LoweredRequirement::from_requirement(
|
||||
requirement,
|
||||
&metadata.name,
|
||||
project_workspace.project_root(),
|
||||
|
@ -99,6 +98,7 @@ impl RequiresDist {
|
|||
project_workspace.workspace(),
|
||||
preview_mode,
|
||||
)
|
||||
.map(LoweredRequirement::into_inner)
|
||||
.map_err(|err| MetadataError::LoweringError(requirement_name.clone(), err))
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
@ -114,7 +114,7 @@ impl RequiresDist {
|
|||
.into_iter()
|
||||
.map(|requirement| {
|
||||
let requirement_name = requirement.name.clone();
|
||||
lower_requirement(
|
||||
LoweredRequirement::from_requirement(
|
||||
requirement,
|
||||
&metadata.name,
|
||||
project_workspace.project_root(),
|
||||
|
@ -122,6 +122,7 @@ impl RequiresDist {
|
|||
project_workspace.workspace(),
|
||||
preview_mode,
|
||||
)
|
||||
.map(LoweredRequirement::into_inner)
|
||||
.map_err(|err| MetadataError::LoweringError(requirement_name.clone(), err))
|
||||
})
|
||||
.collect::<Result<_, _>>()?;
|
||||
|
|
|
@ -11,6 +11,8 @@ workspace = true
|
|||
pep440_rs = { workspace = true }
|
||||
pep508_rs = { workspace = true }
|
||||
pypi-types = { workspace = true }
|
||||
uv-settings = { workspace = true }
|
||||
uv-workspace = { workspace = true }
|
||||
|
||||
fs-err = { workspace = true, features = ["tokio"] }
|
||||
memchr = { workspace = true }
|
||||
|
|
|
@ -1,21 +1,87 @@
|
|||
use memchr::memmem::Finder;
|
||||
use pypi_types::VerbatimParsedUrl;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::BTreeMap;
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use memchr::memmem::Finder;
|
||||
use serde::Deserialize;
|
||||
use thiserror::Error;
|
||||
|
||||
use pep508_rs::PackageName;
|
||||
use pypi_types::VerbatimParsedUrl;
|
||||
use uv_settings::{GlobalOptions, ResolverInstallerOptions};
|
||||
use uv_workspace::pyproject::Source;
|
||||
|
||||
static FINDER: LazyLock<Finder> = LazyLock::new(|| Finder::new(b"# /// script"));
|
||||
|
||||
/// A PEP 723 script, including its [`Pep723Metadata`].
|
||||
#[derive(Debug)]
|
||||
pub struct Pep723Script {
|
||||
pub path: PathBuf,
|
||||
pub metadata: Pep723Metadata,
|
||||
}
|
||||
|
||||
impl Pep723Script {
|
||||
/// Read the PEP 723 `script` metadata from a Python file, if it exists.
|
||||
///
|
||||
/// See: <https://peps.python.org/pep-0723/>
|
||||
pub async fn read(file: impl AsRef<Path>) -> Result<Option<Self>, Pep723Error> {
|
||||
let metadata = Pep723Metadata::read(&file).await?;
|
||||
Ok(metadata.map(|metadata| Self {
|
||||
path: file.as_ref().to_path_buf(),
|
||||
metadata,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/// PEP 723 metadata as parsed from a `script` comment block.
|
||||
///
|
||||
/// See: <https://peps.python.org/pep-0723/>
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct Pep723Metadata {
|
||||
pub dependencies: Option<Vec<pep508_rs::Requirement<VerbatimParsedUrl>>>,
|
||||
pub requires_python: Option<pep440_rs::VersionSpecifiers>,
|
||||
pub tool: Option<Tool>,
|
||||
}
|
||||
|
||||
impl Pep723Metadata {
|
||||
/// Read the PEP 723 `script` metadata from a Python file, if it exists.
|
||||
///
|
||||
/// See: <https://peps.python.org/pep-0723/>
|
||||
pub async fn read(file: impl AsRef<Path>) -> Result<Option<Self>, Pep723Error> {
|
||||
let contents = match fs_err::tokio::read(file).await {
|
||||
Ok(contents) => contents,
|
||||
Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(None),
|
||||
Err(err) => return Err(err.into()),
|
||||
};
|
||||
|
||||
// Extract the `script` tag.
|
||||
let Some(contents) = extract_script_tag(&contents)? else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
// Parse the metadata.
|
||||
let metadata = toml::from_str(&contents)?;
|
||||
|
||||
Ok(Some(metadata))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct Tool {
|
||||
pub uv: Option<ToolUv>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ToolUv {
|
||||
#[serde(flatten)]
|
||||
pub globals: GlobalOptions,
|
||||
#[serde(flatten)]
|
||||
pub top_level: ResolverInstallerOptions,
|
||||
pub sources: Option<BTreeMap<PackageName, Source>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
|
|
|
@ -7,7 +7,7 @@ use uv_configuration::{ConfigSettings, IndexStrategy, KeyringProviderType, Targe
|
|||
use uv_python::{PythonDownloads, PythonPreference, PythonVersion};
|
||||
use uv_resolver::{AnnotationStyle, ExcludeNewer, PrereleaseMode, ResolutionMode};
|
||||
|
||||
use crate::{FilesystemOptions, PipOptions};
|
||||
use crate::{FilesystemOptions, Options, PipOptions};
|
||||
|
||||
pub trait Combine {
|
||||
/// Combine two values, preferring the values in `self`.
|
||||
|
@ -37,6 +37,16 @@ impl Combine for Option<FilesystemOptions> {
|
|||
}
|
||||
}
|
||||
|
||||
impl Combine for Option<Options> {
|
||||
/// Combine the options used in two [`Options`]s. Retains the root of `self`.
|
||||
fn combine(self, other: Option<Options>) -> Option<Options> {
|
||||
match (self, other) {
|
||||
(Some(a), Some(b)) => Some(a.combine(b)),
|
||||
(a, b) => a.or(b),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Combine for Option<PipOptions> {
|
||||
fn combine(self, other: Option<PipOptions>) -> Option<PipOptions> {
|
||||
match (self, other) {
|
||||
|
|
|
@ -154,6 +154,12 @@ impl FilesystemOptions {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<Options> for FilesystemOptions {
|
||||
fn from(options: Options) -> Self {
|
||||
Self(options)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the path to the user configuration directory.
|
||||
///
|
||||
/// This is similar to the `config_dir()` returned by the `dirs` crate, but it uses the
|
||||
|
|
|
@ -69,6 +69,17 @@ pub struct Options {
|
|||
managed: serde::de::IgnoredAny,
|
||||
}
|
||||
|
||||
impl Options {
|
||||
/// Construct an [`Options`] with the given global and top-level settings.
|
||||
pub fn simple(globals: GlobalOptions, top_level: ResolverInstallerOptions) -> Self {
|
||||
Self {
|
||||
globals,
|
||||
top_level,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Global settings, relevant to all invocations.
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone, Default, Deserialize, CombineOptions, OptionsMetadata)]
|
||||
|
|
|
@ -22,7 +22,7 @@ pub(crate) use project::add::add;
|
|||
pub(crate) use project::init::init;
|
||||
pub(crate) use project::lock::lock;
|
||||
pub(crate) use project::remove::remove;
|
||||
pub(crate) use project::run::run;
|
||||
pub(crate) use project::run::{parse_script, run};
|
||||
pub(crate) use project::sync::sync;
|
||||
pub(crate) use project::tree::tree;
|
||||
pub(crate) use python::dir::dir as python_dir;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use std::borrow::Cow;
|
||||
use std::collections::BTreeMap;
|
||||
use std::ffi::OsString;
|
||||
use std::fmt::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
@ -9,11 +10,11 @@ use owo_colors::OwoColorize;
|
|||
use tokio::process::Command;
|
||||
use tracing::debug;
|
||||
|
||||
use pypi_types::Requirement;
|
||||
use uv_cache::Cache;
|
||||
use uv_cli::ExternalCommand;
|
||||
use uv_client::{BaseClientBuilder, Connectivity};
|
||||
use uv_configuration::{Concurrency, ExtrasSpecification, PreviewMode};
|
||||
use uv_distribution::LoweredRequirement;
|
||||
use uv_fs::{PythonExt, Simplified, CWD};
|
||||
use uv_installer::{SatisfiesResult, SitePackages};
|
||||
use uv_normalize::PackageName;
|
||||
|
@ -22,6 +23,7 @@ use uv_python::{
|
|||
PythonEnvironment, PythonInstallation, PythonPreference, PythonRequest, VersionRequest,
|
||||
};
|
||||
use uv_requirements::{RequirementsSource, RequirementsSpecification};
|
||||
use uv_scripts::Pep723Script;
|
||||
use uv_warnings::warn_user_once;
|
||||
use uv_workspace::{DiscoveryOptions, VirtualProject, Workspace, WorkspaceError};
|
||||
|
||||
|
@ -39,6 +41,7 @@ use crate::settings::ResolverInstallerSettings;
|
|||
/// Run a command.
|
||||
#[allow(clippy::fn_params_excessive_bools)]
|
||||
pub(crate) async fn run(
|
||||
script: Option<Pep723Script>,
|
||||
command: ExternalCommand,
|
||||
requirements: Vec<RequirementsSource>,
|
||||
show_resolution: bool,
|
||||
|
@ -87,7 +90,7 @@ pub(crate) async fn run(
|
|||
}
|
||||
|
||||
// Parse the input command.
|
||||
let command = RunCommand::from(command);
|
||||
let command = RunCommand::from(&command);
|
||||
|
||||
// Initialize any shared state.
|
||||
let state = SharedState::default();
|
||||
|
@ -97,88 +100,106 @@ pub(crate) async fn run(
|
|||
|
||||
// Determine whether the command to execute is a PEP 723 script.
|
||||
let temp_dir;
|
||||
let script_interpreter = if let RunCommand::Python(target, _) = &command {
|
||||
if let Some(metadata) = uv_scripts::read_pep723_metadata(&target).await? {
|
||||
writeln!(
|
||||
printer.stderr(),
|
||||
"Reading inline script metadata from: {}",
|
||||
target.user_display().cyan()
|
||||
let script_interpreter = if let Some(script) = script {
|
||||
writeln!(
|
||||
printer.stderr(),
|
||||
"Reading inline script metadata from: {}",
|
||||
script.path.user_display().cyan()
|
||||
)?;
|
||||
|
||||
// (1) Explicit request from user
|
||||
let python_request = if let Some(request) = python.as_deref() {
|
||||
Some(PythonRequest::parse(request))
|
||||
// (2) Request from `.python-version`
|
||||
} else if let Some(request) = request_from_version_file(&CWD).await? {
|
||||
Some(request)
|
||||
// (3) `Requires-Python` in `pyproject.toml`
|
||||
} else {
|
||||
script.metadata.requires_python.map(|requires_python| {
|
||||
PythonRequest::Version(VersionRequest::Range(requires_python))
|
||||
})
|
||||
};
|
||||
|
||||
let client_builder = BaseClientBuilder::new()
|
||||
.connectivity(connectivity)
|
||||
.native_tls(native_tls);
|
||||
|
||||
let interpreter = PythonInstallation::find_or_download(
|
||||
python_request,
|
||||
EnvironmentPreference::Any,
|
||||
python_preference,
|
||||
python_downloads,
|
||||
&client_builder,
|
||||
cache,
|
||||
Some(&download_reporter),
|
||||
)
|
||||
.await?
|
||||
.into_interpreter();
|
||||
|
||||
// Install the script requirements, if necessary. Otherwise, use an isolated environment.
|
||||
if let Some(dependencies) = script.metadata.dependencies {
|
||||
// // Collect any `tool.uv.sources` from the script.
|
||||
let empty = BTreeMap::default();
|
||||
let script_sources = script
|
||||
.metadata
|
||||
.tool
|
||||
.as_ref()
|
||||
.and_then(|tool| tool.uv.as_ref())
|
||||
.and_then(|uv| uv.sources.as_ref())
|
||||
.unwrap_or(&empty);
|
||||
let script_dir = script.path.parent().expect("script path has no parent");
|
||||
|
||||
let requirements = dependencies
|
||||
.into_iter()
|
||||
.map(|requirement| {
|
||||
LoweredRequirement::from_non_workspace_requirement(
|
||||
requirement,
|
||||
script_dir,
|
||||
script_sources,
|
||||
preview,
|
||||
)
|
||||
.map(LoweredRequirement::into_inner)
|
||||
})
|
||||
.collect::<Result<_, _>>()?;
|
||||
let spec = RequirementsSpecification::from_requirements(requirements);
|
||||
let environment = CachedEnvironment::get_or_create(
|
||||
spec,
|
||||
interpreter,
|
||||
&settings,
|
||||
&state,
|
||||
if show_resolution {
|
||||
Box::new(DefaultResolveLogger)
|
||||
} else {
|
||||
Box::new(SummaryResolveLogger)
|
||||
},
|
||||
if show_resolution {
|
||||
Box::new(DefaultInstallLogger)
|
||||
} else {
|
||||
Box::new(SummaryInstallLogger)
|
||||
},
|
||||
preview,
|
||||
connectivity,
|
||||
concurrency,
|
||||
native_tls,
|
||||
cache,
|
||||
printer,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Some(environment.into_interpreter())
|
||||
} else {
|
||||
// Create a virtual environment.
|
||||
temp_dir = cache.environment()?;
|
||||
let environment = uv_virtualenv::create_venv(
|
||||
temp_dir.path(),
|
||||
interpreter,
|
||||
uv_virtualenv::Prompt::None,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
)?;
|
||||
|
||||
// (1) Explicit request from user
|
||||
let python_request = if let Some(request) = python.as_deref() {
|
||||
Some(PythonRequest::parse(request))
|
||||
// (2) Request from `.python-version`
|
||||
} else if let Some(request) = request_from_version_file(&CWD).await? {
|
||||
Some(request)
|
||||
// (3) `Requires-Python` in `pyproject.toml`
|
||||
} else {
|
||||
metadata.requires_python.map(|requires_python| {
|
||||
PythonRequest::Version(VersionRequest::Range(requires_python))
|
||||
})
|
||||
};
|
||||
|
||||
let client_builder = BaseClientBuilder::new()
|
||||
.connectivity(connectivity)
|
||||
.native_tls(native_tls);
|
||||
|
||||
let interpreter = PythonInstallation::find_or_download(
|
||||
python_request,
|
||||
EnvironmentPreference::Any,
|
||||
python_preference,
|
||||
python_downloads,
|
||||
&client_builder,
|
||||
cache,
|
||||
Some(&download_reporter),
|
||||
)
|
||||
.await?
|
||||
.into_interpreter();
|
||||
|
||||
// Install the script requirements, if necessary. Otherwise, use an isolated environment.
|
||||
if let Some(dependencies) = metadata.dependencies {
|
||||
let requirements = dependencies.into_iter().map(Requirement::from).collect();
|
||||
let spec = RequirementsSpecification::from_requirements(requirements);
|
||||
let environment = CachedEnvironment::get_or_create(
|
||||
spec,
|
||||
interpreter,
|
||||
&settings,
|
||||
&state,
|
||||
if show_resolution {
|
||||
Box::new(DefaultResolveLogger)
|
||||
} else {
|
||||
Box::new(SummaryResolveLogger)
|
||||
},
|
||||
if show_resolution {
|
||||
Box::new(DefaultInstallLogger)
|
||||
} else {
|
||||
Box::new(SummaryInstallLogger)
|
||||
},
|
||||
preview,
|
||||
connectivity,
|
||||
concurrency,
|
||||
native_tls,
|
||||
cache,
|
||||
printer,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Some(environment.into_interpreter())
|
||||
} else {
|
||||
// Create a virtual environment.
|
||||
temp_dir = cache.environment()?;
|
||||
let environment = uv_virtualenv::create_venv(
|
||||
temp_dir.path(),
|
||||
interpreter,
|
||||
uv_virtualenv::Prompt::None,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
)?;
|
||||
|
||||
Some(environment.into_interpreter())
|
||||
}
|
||||
} else {
|
||||
None
|
||||
Some(environment.into_interpreter())
|
||||
}
|
||||
} else {
|
||||
None
|
||||
|
@ -590,6 +611,19 @@ pub(crate) async fn run(
|
|||
}
|
||||
}
|
||||
|
||||
/// Read a [`Pep723Script`] from the given command.
|
||||
pub(crate) async fn parse_script(command: &ExternalCommand) -> Result<Option<Pep723Script>> {
|
||||
// Parse the input command.
|
||||
let command = RunCommand::from(command);
|
||||
|
||||
let RunCommand::Python(target, _) = &command else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
// Read the PEP 723 `script` metadata from the target script.
|
||||
Ok(Pep723Script::read(&target).await?)
|
||||
}
|
||||
|
||||
/// Returns `true` if we can skip creating an additional ephemeral environment in `uv run`.
|
||||
fn can_skip_ephemeral(
|
||||
spec: Option<&RequirementsSpecification>,
|
||||
|
@ -682,8 +716,8 @@ impl std::fmt::Display for RunCommand {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<ExternalCommand> for RunCommand {
|
||||
fn from(command: ExternalCommand) -> Self {
|
||||
impl From<&ExternalCommand> for RunCommand {
|
||||
fn from(command: &ExternalCommand) -> Self {
|
||||
let (target, args) = command.split();
|
||||
|
||||
let Some(target) = target else {
|
||||
|
|
|
@ -23,11 +23,12 @@ use uv_cli::{SelfCommand, SelfNamespace};
|
|||
use uv_configuration::Concurrency;
|
||||
use uv_fs::CWD;
|
||||
use uv_requirements::RequirementsSource;
|
||||
use uv_settings::{Combine, FilesystemOptions};
|
||||
use uv_scripts::Pep723Script;
|
||||
use uv_settings::{Combine, FilesystemOptions, Options};
|
||||
use uv_warnings::{warn_user, warn_user_once};
|
||||
use uv_workspace::{DiscoveryOptions, Workspace};
|
||||
|
||||
use crate::commands::{ExitStatus, ToolRunCommand};
|
||||
use crate::commands::{parse_script, ExitStatus, ToolRunCommand};
|
||||
use crate::printer::Printer;
|
||||
use crate::settings::{
|
||||
CacheSettings, GlobalSettings, PipCheckSettings, PipCompileSettings, PipFreezeSettings,
|
||||
|
@ -130,6 +131,27 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
|
|||
project.combine(user)
|
||||
};
|
||||
|
||||
// If the target is a PEP 723 script, parse it.
|
||||
let script = if let Commands::Project(command) = &*cli.command {
|
||||
if let ProjectCommand::Run(uv_cli::RunArgs { command, .. }) = &**command {
|
||||
parse_script(command).await?
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// If the target is a PEP 723 script, merge the metadata into the filesystem metadata.
|
||||
let filesystem = script
|
||||
.as_ref()
|
||||
.map(|script| &script.metadata)
|
||||
.and_then(|metadata| metadata.tool.as_ref())
|
||||
.and_then(|tool| tool.uv.as_ref())
|
||||
.map(|uv| Options::simple(uv.globals.clone(), uv.top_level.clone()))
|
||||
.map(FilesystemOptions::from)
|
||||
.combine(filesystem);
|
||||
|
||||
// Resolve the global settings.
|
||||
let globals = GlobalSettings::resolve(&cli.command, &cli.global_args, filesystem.as_ref());
|
||||
|
||||
|
@ -682,7 +704,7 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
|
|||
.await
|
||||
}
|
||||
Commands::Project(project) => {
|
||||
run_project(project, globals, filesystem, cache, printer).await
|
||||
run_project(project, script, globals, filesystem, cache, printer).await
|
||||
}
|
||||
#[cfg(feature = "self-update")]
|
||||
Commands::Self_(SelfNamespace {
|
||||
|
@ -957,6 +979,7 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
|
|||
/// Run a [`ProjectCommand`].
|
||||
async fn run_project(
|
||||
project_command: Box<ProjectCommand>,
|
||||
script: Option<Pep723Script>,
|
||||
globals: GlobalSettings,
|
||||
filesystem: Option<FilesystemOptions>,
|
||||
cache: Cache,
|
||||
|
@ -1026,7 +1049,8 @@ async fn run_project(
|
|||
)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
commands::run(
|
||||
Box::pin(commands::run(
|
||||
script,
|
||||
args.command,
|
||||
requirements,
|
||||
args.show_resolution || globals.verbose > 0,
|
||||
|
@ -1047,7 +1071,7 @@ async fn run_project(
|
|||
globals.native_tls,
|
||||
&cache,
|
||||
printer,
|
||||
)
|
||||
))
|
||||
.await
|
||||
}
|
||||
ProjectCommand::Sync(args) => {
|
||||
|
|
|
@ -335,6 +335,76 @@ fn run_pep723_script() -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Run a PEP 723-compatible script with `tool.uv` metadata.
|
||||
#[test]
|
||||
fn run_pep723_script_metadata() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
// If the script contains a PEP 723 tag, we should install its requirements.
|
||||
let test_script = context.temp_dir.child("main.py");
|
||||
test_script.write_str(indoc! { r#"
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# "iniconfig>1",
|
||||
# ]
|
||||
#
|
||||
# [tool.uv]
|
||||
# resolution = "lowest-direct"
|
||||
# ///
|
||||
|
||||
import iniconfig
|
||||
"#
|
||||
})?;
|
||||
|
||||
// Running the script should fail without network access.
|
||||
uv_snapshot!(context.filters(), context.run().arg("--preview").arg("main.py"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Reading inline script metadata from: main.py
|
||||
Resolved 1 package in [TIME]
|
||||
Prepared 1 package in [TIME]
|
||||
Installed 1 package in [TIME]
|
||||
+ iniconfig==1.0.1
|
||||
"###);
|
||||
|
||||
// Respect `tool.uv.sources`.
|
||||
let test_script = context.temp_dir.child("main.py");
|
||||
test_script.write_str(indoc! { r#"
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# "uv-public-pypackage",
|
||||
# ]
|
||||
#
|
||||
# [tool.uv.sources]
|
||||
# uv-public-pypackage = { git = "https://github.com/astral-test/uv-public-pypackage", rev = "0dacfd662c64cb4ceb16e6cf65a157a8b715b979" }
|
||||
# ///
|
||||
|
||||
import uv_public_pypackage
|
||||
"#
|
||||
})?;
|
||||
|
||||
// The script should succeed with the specified source.
|
||||
uv_snapshot!(context.filters(), context.run().arg("--preview").arg("main.py"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Reading inline script metadata from: main.py
|
||||
Resolved 1 package in [TIME]
|
||||
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@0dacfd662c64cb4ceb16e6cf65a157a8b715b979)
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// With `managed = false`, we should avoid installing the project itself.
|
||||
#[test]
|
||||
fn run_managed_false() -> Result<()> {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue