mirror of
https://github.com/astral-sh/uv.git
synced 2025-09-26 12:09:12 +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",
|
"serde",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"toml",
|
"toml",
|
||||||
|
"uv-settings",
|
||||||
|
"uv-workspace",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
@ -2,7 +2,7 @@ pub use distribution_database::{DistributionDatabase, HttpArchivePointer, LocalA
|
||||||
pub use download::LocalWheel;
|
pub use download::LocalWheel;
|
||||||
pub use error::Error;
|
pub use error::Error;
|
||||||
pub use index::{BuiltWheelIndex, RegistryWheelIndex};
|
pub use index::{BuiltWheelIndex, RegistryWheelIndex};
|
||||||
pub use metadata::{ArchiveMetadata, Metadata, RequiresDist};
|
pub use metadata::{ArchiveMetadata, LoweredRequirement, Metadata, RequiresDist};
|
||||||
pub use reporter::Reporter;
|
pub use reporter::Reporter;
|
||||||
|
|
||||||
mod archive;
|
mod archive;
|
||||||
|
|
|
@ -17,6 +17,218 @@ use uv_warnings::warn_user_once;
|
||||||
use uv_workspace::pyproject::Source;
|
use uv_workspace::pyproject::Source;
|
||||||
use uv_workspace::Workspace;
|
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
|
/// An error parsing and merging `tool.uv.sources` with
|
||||||
/// `project.{dependencies,optional-dependencies}`.
|
/// `project.{dependencies,optional-dependencies}`.
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
|
@ -27,6 +239,8 @@ pub enum LoweringError {
|
||||||
MoreThanOneGitRef,
|
MoreThanOneGitRef,
|
||||||
#[error("Unable to combine options in `tool.uv.sources`")]
|
#[error("Unable to combine options in `tool.uv.sources`")]
|
||||||
InvalidEntry,
|
InvalidEntry,
|
||||||
|
#[error("Workspace members are not allowed in non-workspace contexts")]
|
||||||
|
WorkspaceMember,
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
InvalidUrl(#[from] url::ParseError),
|
InvalidUrl(#[from] url::ParseError),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
|
@ -47,211 +261,95 @@ pub enum LoweringError {
|
||||||
RelativeTo(io::Error),
|
RelativeTo(io::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Combine `project.dependencies` or `project.optional-dependencies` with `tool.uv.sources`.
|
/// Convert a Git source into a [`RequirementSource`].
|
||||||
pub(crate) fn lower_requirement(
|
fn git_source(
|
||||||
requirement: pep508_rs::Requirement<VerbatimParsedUrl>,
|
git: &Url,
|
||||||
project_name: &PackageName,
|
subdirectory: Option<String>,
|
||||||
project_dir: &Path,
|
rev: Option<String>,
|
||||||
project_sources: &BTreeMap<PackageName, Source>,
|
tag: Option<String>,
|
||||||
workspace: &Workspace,
|
branch: Option<String>,
|
||||||
preview: PreviewMode,
|
) -> Result<RequirementSource, LoweringError> {
|
||||||
) -> Result<Requirement, LoweringError> {
|
let reference = match (rev, tag, branch) {
|
||||||
let source = project_sources
|
(None, None, None) => GitReference::DefaultBranch,
|
||||||
.get(&requirement.name)
|
(Some(rev), None, None) => {
|
||||||
.or(workspace.sources().get(&requirement.name))
|
if rev.starts_with("refs/") {
|
||||||
.cloned();
|
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 =
|
// Create a PEP 508-compatible URL.
|
||||||
// We require that when you use a package that's part of the workspace, ...
|
let mut url = Url::parse(&format!("git+{git}"))?;
|
||||||
!workspace.packages().contains_key(&requirement.name)
|
if let Some(rev) = reference.as_str() {
|
||||||
// ... it must be declared as a workspace dependency (`workspace = true`), ...
|
url.set_path(&format!("{}@{}", url.path(), rev));
|
||||||
|| matches!(
|
}
|
||||||
source,
|
if let Some(subdirectory) = &subdirectory {
|
||||||
Some(Source::Workspace {
|
url.set_fragment(Some(&format!("subdirectory={subdirectory}")));
|
||||||
// By using toml, we technically support `workspace = false`.
|
}
|
||||||
workspace: true,
|
let url = VerbatimUrl::from_url(url);
|
||||||
..
|
|
||||||
})
|
let repository = git.clone();
|
||||||
)
|
|
||||||
// ... except for recursive self-inclusion (extras that activate other extras), e.g.
|
Ok(RequirementSource::Git {
|
||||||
// `framework[machine_learning]` depends on `framework[cuda]`.
|
url,
|
||||||
|| &requirement.name == project_name;
|
repository,
|
||||||
if !workspace_package_declared {
|
reference,
|
||||||
return Err(LoweringError::UndeclaredWorkspacePackage);
|
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 ext = DistExtension::from_path(url.path())
|
||||||
let has_sources = !project_sources.is_empty() || !workspace.sources().is_empty();
|
.map_err(|err| ParsedUrlError::MissingExtensionUrl(url.to_string(), err))?;
|
||||||
// Support recursive editable inclusions.
|
|
||||||
if has_sources && requirement.version_or_url.is_none() && &requirement.name != project_name
|
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!(
|
warn_user_once!(
|
||||||
"Missing version constraint (e.g., a lower bound) for `{}`",
|
"Missing version constraint (e.g., a lower bound) for `{}`",
|
||||||
requirement.name
|
requirement.name
|
||||||
);
|
);
|
||||||
}
|
Ok(RequirementSource::Registry {
|
||||||
return Ok(Requirement::from(requirement));
|
specifier: VersionSpecifiers::empty(),
|
||||||
};
|
|
||||||
|
|
||||||
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,
|
|
||||||
index: Some(index),
|
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 { .. } => {
|
Some(VersionOrUrl::VersionSpecifier(version)) => Ok(RequirementSource::Registry {
|
||||||
// Emit a dedicated error message, which is an improvement over Serde's default error.
|
specifier: version.clone(),
|
||||||
return Err(LoweringError::InvalidEntry);
|
index: Some(index),
|
||||||
}
|
}),
|
||||||
};
|
Some(VersionOrUrl::Url(_)) => Err(LoweringError::ConflictingUrls),
|
||||||
Ok(Requirement {
|
}
|
||||||
name: requirement.name,
|
|
||||||
extras: requirement.extras,
|
|
||||||
marker: requirement.marker,
|
|
||||||
source,
|
|
||||||
origin: requirement.origin,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert a path string to a file or directory source.
|
/// Convert a path string to a file or directory source.
|
||||||
|
|
|
@ -3,14 +3,16 @@ use std::path::Path;
|
||||||
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::metadata::lowering::LoweringError;
|
|
||||||
pub use crate::metadata::requires_dist::RequiresDist;
|
|
||||||
use pep440_rs::{Version, VersionSpecifiers};
|
use pep440_rs::{Version, VersionSpecifiers};
|
||||||
use pypi_types::{HashDigest, Metadata23};
|
use pypi_types::{HashDigest, Metadata23};
|
||||||
use uv_configuration::{PreviewMode, SourceStrategy};
|
use uv_configuration::{PreviewMode, SourceStrategy};
|
||||||
use uv_normalize::{ExtraName, GroupName, PackageName};
|
use uv_normalize::{ExtraName, GroupName, PackageName};
|
||||||
use uv_workspace::WorkspaceError;
|
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 lowering;
|
||||||
mod requires_dist;
|
mod requires_dist;
|
||||||
|
|
||||||
|
|
|
@ -5,8 +5,7 @@ use uv_configuration::{PreviewMode, SourceStrategy};
|
||||||
use uv_normalize::{ExtraName, GroupName, PackageName, DEV_DEPENDENCIES};
|
use uv_normalize::{ExtraName, GroupName, PackageName, DEV_DEPENDENCIES};
|
||||||
use uv_workspace::{DiscoveryOptions, ProjectWorkspace};
|
use uv_workspace::{DiscoveryOptions, ProjectWorkspace};
|
||||||
|
|
||||||
use crate::metadata::lowering::lower_requirement;
|
use crate::metadata::{LoweredRequirement, MetadataError};
|
||||||
use crate::metadata::MetadataError;
|
|
||||||
use crate::Metadata;
|
use crate::Metadata;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
@ -91,7 +90,7 @@ impl RequiresDist {
|
||||||
.cloned()
|
.cloned()
|
||||||
.map(|requirement| {
|
.map(|requirement| {
|
||||||
let requirement_name = requirement.name.clone();
|
let requirement_name = requirement.name.clone();
|
||||||
lower_requirement(
|
LoweredRequirement::from_requirement(
|
||||||
requirement,
|
requirement,
|
||||||
&metadata.name,
|
&metadata.name,
|
||||||
project_workspace.project_root(),
|
project_workspace.project_root(),
|
||||||
|
@ -99,6 +98,7 @@ impl RequiresDist {
|
||||||
project_workspace.workspace(),
|
project_workspace.workspace(),
|
||||||
preview_mode,
|
preview_mode,
|
||||||
)
|
)
|
||||||
|
.map(LoweredRequirement::into_inner)
|
||||||
.map_err(|err| MetadataError::LoweringError(requirement_name.clone(), err))
|
.map_err(|err| MetadataError::LoweringError(requirement_name.clone(), err))
|
||||||
})
|
})
|
||||||
.collect::<Result<Vec<_>, _>>()?;
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
@ -114,7 +114,7 @@ impl RequiresDist {
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|requirement| {
|
.map(|requirement| {
|
||||||
let requirement_name = requirement.name.clone();
|
let requirement_name = requirement.name.clone();
|
||||||
lower_requirement(
|
LoweredRequirement::from_requirement(
|
||||||
requirement,
|
requirement,
|
||||||
&metadata.name,
|
&metadata.name,
|
||||||
project_workspace.project_root(),
|
project_workspace.project_root(),
|
||||||
|
@ -122,6 +122,7 @@ impl RequiresDist {
|
||||||
project_workspace.workspace(),
|
project_workspace.workspace(),
|
||||||
preview_mode,
|
preview_mode,
|
||||||
)
|
)
|
||||||
|
.map(LoweredRequirement::into_inner)
|
||||||
.map_err(|err| MetadataError::LoweringError(requirement_name.clone(), err))
|
.map_err(|err| MetadataError::LoweringError(requirement_name.clone(), err))
|
||||||
})
|
})
|
||||||
.collect::<Result<_, _>>()?;
|
.collect::<Result<_, _>>()?;
|
||||||
|
|
|
@ -11,6 +11,8 @@ workspace = true
|
||||||
pep440_rs = { workspace = true }
|
pep440_rs = { workspace = true }
|
||||||
pep508_rs = { workspace = true }
|
pep508_rs = { workspace = true }
|
||||||
pypi-types = { workspace = true }
|
pypi-types = { workspace = true }
|
||||||
|
uv-settings = { workspace = true }
|
||||||
|
uv-workspace = { workspace = true }
|
||||||
|
|
||||||
fs-err = { workspace = true, features = ["tokio"] }
|
fs-err = { workspace = true, features = ["tokio"] }
|
||||||
memchr = { workspace = true }
|
memchr = { workspace = true }
|
||||||
|
|
|
@ -1,21 +1,87 @@
|
||||||
use memchr::memmem::Finder;
|
use std::collections::BTreeMap;
|
||||||
use pypi_types::VerbatimParsedUrl;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::path::Path;
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
|
use memchr::memmem::Finder;
|
||||||
|
use serde::Deserialize;
|
||||||
use thiserror::Error;
|
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"));
|
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.
|
/// PEP 723 metadata as parsed from a `script` comment block.
|
||||||
///
|
///
|
||||||
/// See: <https://peps.python.org/pep-0723/>
|
/// See: <https://peps.python.org/pep-0723/>
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
pub struct Pep723Metadata {
|
pub struct Pep723Metadata {
|
||||||
pub dependencies: Option<Vec<pep508_rs::Requirement<VerbatimParsedUrl>>>,
|
pub dependencies: Option<Vec<pep508_rs::Requirement<VerbatimParsedUrl>>>,
|
||||||
pub requires_python: Option<pep440_rs::VersionSpecifiers>,
|
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)]
|
#[derive(Debug, Error)]
|
||||||
|
|
|
@ -7,7 +7,7 @@ use uv_configuration::{ConfigSettings, IndexStrategy, KeyringProviderType, Targe
|
||||||
use uv_python::{PythonDownloads, PythonPreference, PythonVersion};
|
use uv_python::{PythonDownloads, PythonPreference, PythonVersion};
|
||||||
use uv_resolver::{AnnotationStyle, ExcludeNewer, PrereleaseMode, ResolutionMode};
|
use uv_resolver::{AnnotationStyle, ExcludeNewer, PrereleaseMode, ResolutionMode};
|
||||||
|
|
||||||
use crate::{FilesystemOptions, PipOptions};
|
use crate::{FilesystemOptions, Options, PipOptions};
|
||||||
|
|
||||||
pub trait Combine {
|
pub trait Combine {
|
||||||
/// Combine two values, preferring the values in `self`.
|
/// 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> {
|
impl Combine for Option<PipOptions> {
|
||||||
fn combine(self, other: Option<PipOptions>) -> Option<PipOptions> {
|
fn combine(self, other: Option<PipOptions>) -> Option<PipOptions> {
|
||||||
match (self, other) {
|
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.
|
/// Returns the path to the user configuration directory.
|
||||||
///
|
///
|
||||||
/// This is similar to the `config_dir()` returned by the `dirs` crate, but it uses the
|
/// 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,
|
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.
|
/// Global settings, relevant to all invocations.
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
#[derive(Debug, Clone, Default, Deserialize, CombineOptions, OptionsMetadata)]
|
#[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::init::init;
|
||||||
pub(crate) use project::lock::lock;
|
pub(crate) use project::lock::lock;
|
||||||
pub(crate) use project::remove::remove;
|
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::sync::sync;
|
||||||
pub(crate) use project::tree::tree;
|
pub(crate) use project::tree::tree;
|
||||||
pub(crate) use python::dir::dir as python_dir;
|
pub(crate) use python::dir::dir as python_dir;
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
use std::collections::BTreeMap;
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
@ -9,11 +10,11 @@ use owo_colors::OwoColorize;
|
||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
|
||||||
use pypi_types::Requirement;
|
|
||||||
use uv_cache::Cache;
|
use uv_cache::Cache;
|
||||||
use uv_cli::ExternalCommand;
|
use uv_cli::ExternalCommand;
|
||||||
use uv_client::{BaseClientBuilder, Connectivity};
|
use uv_client::{BaseClientBuilder, Connectivity};
|
||||||
use uv_configuration::{Concurrency, ExtrasSpecification, PreviewMode};
|
use uv_configuration::{Concurrency, ExtrasSpecification, PreviewMode};
|
||||||
|
use uv_distribution::LoweredRequirement;
|
||||||
use uv_fs::{PythonExt, Simplified, CWD};
|
use uv_fs::{PythonExt, Simplified, CWD};
|
||||||
use uv_installer::{SatisfiesResult, SitePackages};
|
use uv_installer::{SatisfiesResult, SitePackages};
|
||||||
use uv_normalize::PackageName;
|
use uv_normalize::PackageName;
|
||||||
|
@ -22,6 +23,7 @@ use uv_python::{
|
||||||
PythonEnvironment, PythonInstallation, PythonPreference, PythonRequest, VersionRequest,
|
PythonEnvironment, PythonInstallation, PythonPreference, PythonRequest, VersionRequest,
|
||||||
};
|
};
|
||||||
use uv_requirements::{RequirementsSource, RequirementsSpecification};
|
use uv_requirements::{RequirementsSource, RequirementsSpecification};
|
||||||
|
use uv_scripts::Pep723Script;
|
||||||
use uv_warnings::warn_user_once;
|
use uv_warnings::warn_user_once;
|
||||||
use uv_workspace::{DiscoveryOptions, VirtualProject, Workspace, WorkspaceError};
|
use uv_workspace::{DiscoveryOptions, VirtualProject, Workspace, WorkspaceError};
|
||||||
|
|
||||||
|
@ -39,6 +41,7 @@ use crate::settings::ResolverInstallerSettings;
|
||||||
/// Run a command.
|
/// Run a command.
|
||||||
#[allow(clippy::fn_params_excessive_bools)]
|
#[allow(clippy::fn_params_excessive_bools)]
|
||||||
pub(crate) async fn run(
|
pub(crate) async fn run(
|
||||||
|
script: Option<Pep723Script>,
|
||||||
command: ExternalCommand,
|
command: ExternalCommand,
|
||||||
requirements: Vec<RequirementsSource>,
|
requirements: Vec<RequirementsSource>,
|
||||||
show_resolution: bool,
|
show_resolution: bool,
|
||||||
|
@ -87,7 +90,7 @@ pub(crate) async fn run(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse the input command.
|
// Parse the input command.
|
||||||
let command = RunCommand::from(command);
|
let command = RunCommand::from(&command);
|
||||||
|
|
||||||
// Initialize any shared state.
|
// Initialize any shared state.
|
||||||
let state = SharedState::default();
|
let state = SharedState::default();
|
||||||
|
@ -97,88 +100,106 @@ pub(crate) async fn run(
|
||||||
|
|
||||||
// Determine whether the command to execute is a PEP 723 script.
|
// Determine whether the command to execute is a PEP 723 script.
|
||||||
let temp_dir;
|
let temp_dir;
|
||||||
let script_interpreter = if let RunCommand::Python(target, _) = &command {
|
let script_interpreter = if let Some(script) = script {
|
||||||
if let Some(metadata) = uv_scripts::read_pep723_metadata(&target).await? {
|
writeln!(
|
||||||
writeln!(
|
printer.stderr(),
|
||||||
printer.stderr(),
|
"Reading inline script metadata from: {}",
|
||||||
"Reading inline script metadata from: {}",
|
script.path.user_display().cyan()
|
||||||
target.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
|
Some(environment.into_interpreter())
|
||||||
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
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
None
|
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`.
|
/// Returns `true` if we can skip creating an additional ephemeral environment in `uv run`.
|
||||||
fn can_skip_ephemeral(
|
fn can_skip_ephemeral(
|
||||||
spec: Option<&RequirementsSpecification>,
|
spec: Option<&RequirementsSpecification>,
|
||||||
|
@ -682,8 +716,8 @@ impl std::fmt::Display for RunCommand {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<ExternalCommand> for RunCommand {
|
impl From<&ExternalCommand> for RunCommand {
|
||||||
fn from(command: ExternalCommand) -> Self {
|
fn from(command: &ExternalCommand) -> Self {
|
||||||
let (target, args) = command.split();
|
let (target, args) = command.split();
|
||||||
|
|
||||||
let Some(target) = target else {
|
let Some(target) = target else {
|
||||||
|
|
|
@ -23,11 +23,12 @@ use uv_cli::{SelfCommand, SelfNamespace};
|
||||||
use uv_configuration::Concurrency;
|
use uv_configuration::Concurrency;
|
||||||
use uv_fs::CWD;
|
use uv_fs::CWD;
|
||||||
use uv_requirements::RequirementsSource;
|
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_warnings::{warn_user, warn_user_once};
|
||||||
use uv_workspace::{DiscoveryOptions, Workspace};
|
use uv_workspace::{DiscoveryOptions, Workspace};
|
||||||
|
|
||||||
use crate::commands::{ExitStatus, ToolRunCommand};
|
use crate::commands::{parse_script, ExitStatus, ToolRunCommand};
|
||||||
use crate::printer::Printer;
|
use crate::printer::Printer;
|
||||||
use crate::settings::{
|
use crate::settings::{
|
||||||
CacheSettings, GlobalSettings, PipCheckSettings, PipCompileSettings, PipFreezeSettings,
|
CacheSettings, GlobalSettings, PipCheckSettings, PipCompileSettings, PipFreezeSettings,
|
||||||
|
@ -130,6 +131,27 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
|
||||||
project.combine(user)
|
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.
|
// Resolve the global settings.
|
||||||
let globals = GlobalSettings::resolve(&cli.command, &cli.global_args, filesystem.as_ref());
|
let globals = GlobalSettings::resolve(&cli.command, &cli.global_args, filesystem.as_ref());
|
||||||
|
|
||||||
|
@ -682,7 +704,7 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
Commands::Project(project) => {
|
Commands::Project(project) => {
|
||||||
run_project(project, globals, filesystem, cache, printer).await
|
run_project(project, script, globals, filesystem, cache, printer).await
|
||||||
}
|
}
|
||||||
#[cfg(feature = "self-update")]
|
#[cfg(feature = "self-update")]
|
||||||
Commands::Self_(SelfNamespace {
|
Commands::Self_(SelfNamespace {
|
||||||
|
@ -957,6 +979,7 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
|
||||||
/// Run a [`ProjectCommand`].
|
/// Run a [`ProjectCommand`].
|
||||||
async fn run_project(
|
async fn run_project(
|
||||||
project_command: Box<ProjectCommand>,
|
project_command: Box<ProjectCommand>,
|
||||||
|
script: Option<Pep723Script>,
|
||||||
globals: GlobalSettings,
|
globals: GlobalSettings,
|
||||||
filesystem: Option<FilesystemOptions>,
|
filesystem: Option<FilesystemOptions>,
|
||||||
cache: Cache,
|
cache: Cache,
|
||||||
|
@ -1026,7 +1049,8 @@ async fn run_project(
|
||||||
)
|
)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
commands::run(
|
Box::pin(commands::run(
|
||||||
|
script,
|
||||||
args.command,
|
args.command,
|
||||||
requirements,
|
requirements,
|
||||||
args.show_resolution || globals.verbose > 0,
|
args.show_resolution || globals.verbose > 0,
|
||||||
|
@ -1047,7 +1071,7 @@ async fn run_project(
|
||||||
globals.native_tls,
|
globals.native_tls,
|
||||||
&cache,
|
&cache,
|
||||||
printer,
|
printer,
|
||||||
)
|
))
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
ProjectCommand::Sync(args) => {
|
ProjectCommand::Sync(args) => {
|
||||||
|
|
|
@ -335,6 +335,76 @@ fn run_pep723_script() -> Result<()> {
|
||||||
Ok(())
|
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.
|
/// With `managed = false`, we should avoid installing the project itself.
|
||||||
#[test]
|
#[test]
|
||||||
fn run_managed_false() -> Result<()> {
|
fn run_managed_false() -> Result<()> {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue