mirror of
https://github.com/astral-sh/uv
synced 2026-06-21 13:47:25 +00:00
Add --script to uv check and uv metadata (#19860)
Like other `--script` commands this tells uv to ignore the current workspace and only handle the PEP 723 script at the given path, with its own lock and venv.
This commit is contained in:
@@ -5237,6 +5237,29 @@ pub struct FormatArgs {
|
||||
|
||||
#[derive(Args)]
|
||||
pub struct CheckArgs {
|
||||
/// Run checks for the specified PEP 723 Python script, rather than the current project.
|
||||
///
|
||||
/// If provided, uv will use the dependencies based on the script's inline metadata table, in
|
||||
/// adherence with PEP 723.
|
||||
#[arg(
|
||||
long,
|
||||
conflicts_with = "extra",
|
||||
conflicts_with = "all_extras",
|
||||
conflicts_with = "no_extra",
|
||||
conflicts_with = "no_all_extras",
|
||||
conflicts_with = "dev",
|
||||
conflicts_with = "no_dev",
|
||||
conflicts_with = "only_dev",
|
||||
conflicts_with = "group",
|
||||
conflicts_with = "no_group",
|
||||
conflicts_with = "no_default_groups",
|
||||
conflicts_with = "only_group",
|
||||
conflicts_with = "all_groups",
|
||||
conflicts_with = "no_project",
|
||||
value_hint = ValueHint::FilePath,
|
||||
)]
|
||||
pub script: Option<PathBuf>,
|
||||
|
||||
/// Include optional dependencies from the specified extra name.
|
||||
///
|
||||
/// May be provided more than once.
|
||||
@@ -8255,6 +8278,13 @@ pub enum WorkspaceCommand {
|
||||
}
|
||||
#[derive(Args)]
|
||||
pub struct MetadataArgs {
|
||||
/// View metadata for the specified PEP 723 Python script, rather than the current workspace.
|
||||
///
|
||||
/// If provided, uv will resolve the dependencies based on the script's inline metadata table,
|
||||
/// in adherence with PEP 723.
|
||||
#[arg(long, value_hint = ValueHint::FilePath)]
|
||||
pub script: Option<PathBuf>,
|
||||
|
||||
/// Check if the lockfile is up-to-date [env: UV_LOCKED=]
|
||||
///
|
||||
/// Asserts that the `uv.lock` would remain unchanged after a resolution. If the lockfile is
|
||||
|
||||
@@ -66,6 +66,9 @@ pub struct Metadata {
|
||||
/// Information about the synchronized environment, when `--sync` was used.
|
||||
#[serde(skip_serializing_if = "Option::is_none", default)]
|
||||
environment: Option<MetadataEnvironment>,
|
||||
/// Information about the script root, when metadata was requested for a script.
|
||||
#[serde(skip_serializing_if = "Option::is_none", default)]
|
||||
script: Option<MetadataScript>,
|
||||
/// The version of python required by the workspace
|
||||
///
|
||||
/// Every `marker` we emit implicitly assumes this constraint to keep things clean
|
||||
@@ -108,6 +111,15 @@ struct MetadataEnvironment {
|
||||
root: PortablePathBuf,
|
||||
}
|
||||
|
||||
/// The script entry-point.
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
struct MetadataScript {
|
||||
/// Absolute path to the script.
|
||||
path: PortablePathBuf,
|
||||
/// Key for the script's node in the `resolution` graph.
|
||||
id: MetadataNodeIdFlat,
|
||||
}
|
||||
|
||||
/// Info for looking up workspace members, most information is stored in the node behind `id`
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
struct MetadataWorkspaceMember {
|
||||
@@ -126,10 +138,11 @@ struct MetadataModuleOwner {
|
||||
package_id: MetadataNodeIdFlat,
|
||||
}
|
||||
|
||||
/// A node in the dependency graph
|
||||
/// A node in the dependency graph.
|
||||
///
|
||||
/// There are 4 kinds of nodes:
|
||||
/// There are 5 kinds of nodes:
|
||||
///
|
||||
/// * scripts: `script+/workspace/script.py`
|
||||
/// * packages: `mypackage==1.0.0@registry+https://pypi.org/simple`
|
||||
/// * extras: `mypackage[myextra]==1.0.0@registry+https://pypi.org/simple`
|
||||
/// * groups: `mypackage:mygroup==1.0.0@registry+https://pypi.org/simple`
|
||||
@@ -216,6 +229,15 @@ impl MetadataNode {
|
||||
Self::new(MetadataNodeId::from_package_id(workspace_root, id, kind))
|
||||
}
|
||||
|
||||
fn from_script(path: PortablePathBuf, dependencies: Vec<MetadataDependency>) -> Self {
|
||||
let mut node = Self::new(MetadataNodeId::Script(MetadataScriptNodeId {
|
||||
kind: MetadataScriptNodeKind::Script,
|
||||
path,
|
||||
}));
|
||||
node.dependencies = dependencies;
|
||||
node
|
||||
}
|
||||
|
||||
fn add_dependency(&mut self, workspace_root: &PortablePathBuf, dependency: &Dependency) {
|
||||
let extras = dependency.extra();
|
||||
if extras.is_empty() {
|
||||
@@ -244,11 +266,79 @@ impl MetadataNode {
|
||||
}
|
||||
}
|
||||
|
||||
/// The unique key for every node in the graph
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
enum MetadataScriptNodeKind {
|
||||
Script,
|
||||
}
|
||||
|
||||
fn script_root_dependencies(
|
||||
workspace_root: &PortablePathBuf,
|
||||
lock: &Lock,
|
||||
) -> Vec<MetadataDependency> {
|
||||
let mut dependencies = Vec::new();
|
||||
|
||||
// Root requirements retain names, extras, and markers rather than resolved package IDs. Match
|
||||
// them to the locked packages using the same name and fork-marker logic as lock export.
|
||||
for requirement in lock.requirements() {
|
||||
for package in lock
|
||||
.packages()
|
||||
.iter()
|
||||
.filter(|package| package.name() == &requirement.name)
|
||||
{
|
||||
let Some(marker) = lock.root_requirement_marker(requirement, package) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let marker = marker.try_to_string();
|
||||
let mut has_extra_node = false;
|
||||
for extra in requirement
|
||||
.extras
|
||||
.iter()
|
||||
.filter(|extra| package.optional_dependencies.contains_key(*extra))
|
||||
{
|
||||
let id = MetadataNodeId::from_package_id(
|
||||
workspace_root,
|
||||
&package.id,
|
||||
MetadataNodeKind::Extra(extra.clone()),
|
||||
);
|
||||
dependencies.push(MetadataDependency {
|
||||
id: id.to_flat(),
|
||||
marker: marker.clone(),
|
||||
});
|
||||
has_extra_node = true;
|
||||
}
|
||||
|
||||
if !has_extra_node {
|
||||
let id = MetadataNodeId::from_package_id(
|
||||
workspace_root,
|
||||
&package.id,
|
||||
MetadataNodeKind::Package,
|
||||
);
|
||||
dependencies.push(MetadataDependency {
|
||||
id: id.to_flat(),
|
||||
marker,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies
|
||||
}
|
||||
|
||||
/// The unique key for every node in the graph.
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
#[serde(untagged)]
|
||||
enum MetadataNodeId {
|
||||
Package(MetadataPackageNodeId),
|
||||
Script(MetadataScriptNodeId),
|
||||
}
|
||||
|
||||
/// The unique key for a package-derived node.
|
||||
///
|
||||
/// (It's not entirely clear to me that two nodes can differ only by `source` but it doesn't hurt.)
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
struct MetadataNodeId {
|
||||
struct MetadataPackageNodeId {
|
||||
/// The name of the package
|
||||
name: PackageName,
|
||||
/// The version of the package, if any could be found (source trees may have no version)
|
||||
@@ -260,11 +350,17 @@ struct MetadataNodeId {
|
||||
kind: MetadataNodeKind,
|
||||
}
|
||||
|
||||
/// The unique key for a script node.
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
struct MetadataScriptNodeId {
|
||||
kind: MetadataScriptNodeKind,
|
||||
/// Absolute path to the script.
|
||||
path: PortablePathBuf,
|
||||
}
|
||||
|
||||
/// This is intended to be an opaque unique id for referring to a node
|
||||
///
|
||||
/// It's human readable for convenience but parsing it or relying on it is inadvisable.
|
||||
/// As currently implemented this is just a concatenation of the 4 fields in `MetadataNodeId`
|
||||
/// which every node includes, so parsing it is just making more work for yourself.
|
||||
type MetadataNodeIdFlat = String;
|
||||
|
||||
impl MetadataNodeId {
|
||||
@@ -277,11 +373,18 @@ impl MetadataNodeId {
|
||||
let version = id.version.clone();
|
||||
let source = MetadataSource::from_source(workspace_root, id.source.clone());
|
||||
|
||||
Self {
|
||||
Self::Package(MetadataPackageNodeId {
|
||||
name,
|
||||
version,
|
||||
source,
|
||||
kind,
|
||||
})
|
||||
}
|
||||
|
||||
fn as_package(&self) -> Option<&MetadataPackageNodeId> {
|
||||
match self {
|
||||
Self::Package(package) => Some(package),
|
||||
Self::Script(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -292,9 +395,16 @@ impl MetadataNodeId {
|
||||
|
||||
impl Display for MetadataNodeId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match &self.version {
|
||||
Some(version) => write!(f, "{}{}=={version}@{}", self.name, self.kind, self.source),
|
||||
None => write!(f, "{}{}@{}", self.name, self.kind, self.source),
|
||||
match self {
|
||||
Self::Package(package) => match &package.version {
|
||||
Some(version) => write!(
|
||||
f,
|
||||
"{}{}=={version}@{}",
|
||||
package.name, package.kind, package.source
|
||||
),
|
||||
None => write!(f, "{}{}@{}", package.name, package.kind, package.source),
|
||||
},
|
||||
Self::Script(script) => write!(f, "script+{}", script.path),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -704,11 +814,11 @@ impl MetadataConflictItem {
|
||||
.iter()
|
||||
.find(|member| &member.name == item.package())
|
||||
.and_then(|member| {
|
||||
let package_node = resolve.get(&member.id)?;
|
||||
let id = MetadataNodeId {
|
||||
let package_id = resolve.get(&member.id)?.id.as_package()?;
|
||||
let id = MetadataNodeId::Package(MetadataPackageNodeId {
|
||||
kind: kind.to_node_kind(),
|
||||
..package_node.id.clone()
|
||||
};
|
||||
..package_id.clone()
|
||||
});
|
||||
Some(id.to_flat())
|
||||
});
|
||||
Self {
|
||||
@@ -745,11 +855,36 @@ impl MetadataConflictKind {
|
||||
}
|
||||
|
||||
impl Metadata {
|
||||
/// Construct a [`PylockToml`] from a uv lockfile.
|
||||
/// Construct [`Metadata`] for a workspace from a uv lockfile.
|
||||
pub fn from_lock(workspace: &Workspace, lock: &Lock) -> Result<Self, MetadataError> {
|
||||
Ok(Self::from_lock_target(
|
||||
workspace.install_path(),
|
||||
Some(workspace),
|
||||
None,
|
||||
lock,
|
||||
))
|
||||
}
|
||||
|
||||
/// Construct [`Metadata`] for a script from a uv lockfile.
|
||||
pub fn from_script(script_path: &Path, lock: &Lock) -> Result<Self, MetadataError> {
|
||||
let workspace_root = script_path.parent().unwrap_or_else(|| Path::new(""));
|
||||
Ok(Self::from_lock_target(
|
||||
workspace_root,
|
||||
None,
|
||||
Some(script_path),
|
||||
lock,
|
||||
))
|
||||
}
|
||||
|
||||
fn from_lock_target(
|
||||
workspace_root: &Path,
|
||||
workspace: Option<&Workspace>,
|
||||
script_path: Option<&Path>,
|
||||
lock: &Lock,
|
||||
) -> Self {
|
||||
let mut resolve = BTreeMap::new();
|
||||
let mut members = Vec::new();
|
||||
let workspace_root = PortablePathBuf::from(workspace.install_path().as_path());
|
||||
let workspace_root = PortablePathBuf::from(workspace_root);
|
||||
|
||||
for lock_package in lock.packages() {
|
||||
let mut meta_package = MetadataNode::from_package_id(
|
||||
@@ -808,9 +943,11 @@ impl Metadata {
|
||||
}
|
||||
|
||||
// Register this package if it appears to be a workspace member
|
||||
if let Some(workspace_package) = workspace.packages().get(lock_package.name()) {
|
||||
if let Some(workspace_package) =
|
||||
workspace.and_then(|workspace| workspace.packages().get(lock_package.name()))
|
||||
{
|
||||
let member = MetadataWorkspaceMember {
|
||||
name: meta_package.id.name.clone(),
|
||||
name: lock_package.name().clone(),
|
||||
path: normalize_workspace_relative_path(
|
||||
&workspace_root,
|
||||
workspace_package.root().as_path(),
|
||||
@@ -834,20 +971,31 @@ impl Metadata {
|
||||
resolve.insert(meta_package.id.to_flat(), meta_package);
|
||||
}
|
||||
|
||||
let script = script_path.map(|path| {
|
||||
let path = PortablePathBuf::from(path);
|
||||
let node = MetadataNode::from_script(
|
||||
path.clone(),
|
||||
script_root_dependencies(&workspace_root, lock),
|
||||
);
|
||||
let id = node.id.to_flat();
|
||||
resolve.insert(id.clone(), node);
|
||||
MetadataScript { path, id }
|
||||
});
|
||||
let conflicts = MetadataConflicts::from_conflicts(&members, &resolve, &lock.conflicts);
|
||||
|
||||
Ok(Self {
|
||||
Self {
|
||||
schema: SchemaReport {
|
||||
version: SchemaVersion::Preview,
|
||||
},
|
||||
conflicts,
|
||||
environment: None,
|
||||
script,
|
||||
module_owners: BTreeMap::new(),
|
||||
workspace_root,
|
||||
requires_python: lock.requires_python.clone(),
|
||||
members,
|
||||
resolution: resolve,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn package_node_id(
|
||||
@@ -855,12 +1003,12 @@ impl Metadata {
|
||||
dist: &ResolvedDist,
|
||||
) -> Result<String, MetadataError> {
|
||||
let source = Source::from_resolved_dist(dist, workspace_root.as_ref())?;
|
||||
Ok(MetadataNodeId {
|
||||
Ok(MetadataNodeId::Package(MetadataPackageNodeId {
|
||||
name: dist.name().clone(),
|
||||
version: dist.version().cloned(),
|
||||
source: MetadataSource::from_source(workspace_root, source),
|
||||
kind: MetadataNodeKind::Package,
|
||||
}
|
||||
})
|
||||
.to_flat())
|
||||
}
|
||||
|
||||
|
||||
@@ -209,25 +209,12 @@ impl<'lock> ExportableRequirements<'lock> {
|
||||
|
||||
for requirement in root_requirements {
|
||||
for dist in by_name.get(&requirement.name).into_iter().flatten() {
|
||||
// Determine whether this entry is "relevant" for the requirement, by intersecting
|
||||
// the markers.
|
||||
let marker = if dist.fork_markers.is_empty() {
|
||||
requirement.marker
|
||||
} else {
|
||||
let mut combined = MarkerTree::FALSE;
|
||||
for fork_marker in &dist.fork_markers {
|
||||
combined.or(fork_marker.pep508());
|
||||
}
|
||||
combined.and(requirement.marker);
|
||||
combined
|
||||
};
|
||||
|
||||
if marker.is_false() {
|
||||
// Determine whether this entry is relevant for the requirement by
|
||||
// intersecting and simplifying the markers.
|
||||
let Some(marker) = target.lock().root_requirement_marker(requirement, dist)
|
||||
else {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Simplify the marker.
|
||||
let marker = target.lock().simplify_environment(marker);
|
||||
};
|
||||
|
||||
// Add the dependency to the graph and get its index.
|
||||
let dep_index = *inverse
|
||||
|
||||
@@ -765,11 +765,32 @@ impl Lock {
|
||||
&self.manifest.members
|
||||
}
|
||||
|
||||
/// Returns the dependency groups that were used to generate this lock.
|
||||
/// Returns the root requirements that were used to generate this lock.
|
||||
fn requirements(&self) -> &BTreeSet<Requirement> {
|
||||
&self.manifest.requirements
|
||||
}
|
||||
|
||||
/// Intersect a requirement marker with the forks that contain a package, then simplify it
|
||||
/// under the lockfile's Python requirement.
|
||||
pub(crate) fn root_requirement_marker(
|
||||
&self,
|
||||
requirement: &Requirement,
|
||||
package: &Package,
|
||||
) -> Option<MarkerTree> {
|
||||
let marker = if package.fork_markers.is_empty() {
|
||||
requirement.marker
|
||||
} else {
|
||||
let mut combined = MarkerTree::FALSE;
|
||||
for fork_marker in &package.fork_markers {
|
||||
combined.or(fork_marker.pep508());
|
||||
}
|
||||
combined.and(requirement.marker);
|
||||
combined
|
||||
};
|
||||
|
||||
(!marker.is_false()).then(|| self.simplify_environment(marker))
|
||||
}
|
||||
|
||||
/// Returns the dependency groups that were used to generate this lock.
|
||||
pub(crate) fn dependency_groups(&self) -> &BTreeMap<GroupName, BTreeSet<Requirement>> {
|
||||
&self.manifest.dependency_groups
|
||||
|
||||
@@ -14,6 +14,7 @@ use uv_preview::{Preview, PreviewFeature};
|
||||
use uv_python::{
|
||||
EnvironmentPreference, PythonDownloads, PythonInstallation, PythonPreference, PythonRequest,
|
||||
};
|
||||
use uv_scripts::Pep723Script;
|
||||
use uv_settings::{MalwareCheckSettings, PythonInstallMirrors};
|
||||
use uv_warnings::warn_user;
|
||||
use uv_workspace::{DiscoveryOptions, VirtualProject, WorkspaceCache, WorkspaceErrorKind};
|
||||
@@ -22,9 +23,10 @@ use crate::commands::pip::loggers::{SummaryInstallLogger, SummaryResolveLogger};
|
||||
use crate::commands::pip::operations::Modifications;
|
||||
use crate::commands::project::install_target::InstallTarget;
|
||||
use crate::commands::project::lock::LockMode;
|
||||
use crate::commands::project::lock_target::LockTarget;
|
||||
use crate::commands::project::{
|
||||
ProjectEnvironment, ProjectError, ProjectInterpreter, UniversalState, WorkspacePython,
|
||||
default_dependency_groups, validate_project_requires_python,
|
||||
ProjectEnvironment, ProjectError, ProjectInterpreter, ScriptEnvironment, ScriptInterpreter,
|
||||
UniversalState, WorkspacePython, default_dependency_groups, validate_project_requires_python,
|
||||
};
|
||||
use crate::commands::reporters::PythonDownloadReporter;
|
||||
use crate::commands::{ExitStatus, diagnostics, project};
|
||||
@@ -49,6 +51,7 @@ pub(crate) async fn check(
|
||||
settings: ResolverInstallerSettings,
|
||||
ty_version: Option<String>,
|
||||
show_version: bool,
|
||||
script: Option<Pep723Script>,
|
||||
client_builder: BaseClientBuilder<'_>,
|
||||
python_preference: PythonPreference,
|
||||
python_downloads: PythonDownloads,
|
||||
@@ -70,7 +73,7 @@ pub(crate) async fn check(
|
||||
}
|
||||
|
||||
// Discover the project.
|
||||
let project = if no_project {
|
||||
let project = if no_project || script.is_some() {
|
||||
None
|
||||
} else {
|
||||
match VirtualProject::discover(
|
||||
@@ -113,7 +116,7 @@ pub(crate) async fn check(
|
||||
if no_sync {
|
||||
warn_user!("`--no-sync` has no effect when used alongside `--no-project`");
|
||||
}
|
||||
} else if project.is_none() {
|
||||
} else if project.is_none() && script.is_none() {
|
||||
for flag in extras.history().as_flags_pretty() {
|
||||
warn_user!("`{flag}` has no effect when used outside of a project");
|
||||
}
|
||||
@@ -131,9 +134,11 @@ pub(crate) async fn check(
|
||||
}
|
||||
}
|
||||
|
||||
let target_dir = project
|
||||
let target_dir = script
|
||||
.as_ref()
|
||||
.map(|p| p.root().to_owned())
|
||||
.and_then(|script| script.path.parent())
|
||||
.map(Path::to_path_buf)
|
||||
.or_else(|| project.as_ref().map(|project| project.root().to_owned()))
|
||||
.unwrap_or_else(|| project_dir.to_owned());
|
||||
|
||||
let groups = if let Some(project) = &project {
|
||||
@@ -147,45 +152,64 @@ pub(crate) async fn check(
|
||||
let isolated_venv = if isolated {
|
||||
debug!("Creating isolated virtual environment");
|
||||
|
||||
let workspace = project.as_ref().map(VirtualProject::workspace);
|
||||
let WorkspacePython {
|
||||
source,
|
||||
python_request,
|
||||
requires_python,
|
||||
} = WorkspacePython::from_request(
|
||||
python.as_deref().map(PythonRequest::parse),
|
||||
workspace,
|
||||
&groups,
|
||||
project_dir,
|
||||
no_config,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let reporter = PythonDownloadReporter::single(printer);
|
||||
let interpreter = PythonInstallation::find_or_download(
|
||||
python_request.as_ref(),
|
||||
EnvironmentPreference::Any,
|
||||
python_preference,
|
||||
python_downloads,
|
||||
&client_builder,
|
||||
cache,
|
||||
Some(&reporter),
|
||||
install_mirrors.python_install_mirror.as_deref(),
|
||||
install_mirrors.pypy_install_mirror.as_deref(),
|
||||
install_mirrors.python_downloads_json_url.as_deref(),
|
||||
)
|
||||
.await?
|
||||
.into_interpreter();
|
||||
|
||||
if let Some(requires_python) = requires_python.as_ref() {
|
||||
validate_project_requires_python(
|
||||
&interpreter,
|
||||
let interpreter = if let Some(script) = script.as_ref() {
|
||||
ScriptInterpreter::discover(
|
||||
script.into(),
|
||||
python.as_deref().map(PythonRequest::parse),
|
||||
&client_builder,
|
||||
python_preference,
|
||||
python_downloads,
|
||||
&install_mirrors,
|
||||
false,
|
||||
no_config,
|
||||
Some(false),
|
||||
cache,
|
||||
printer,
|
||||
)
|
||||
.await?
|
||||
.into_interpreter()
|
||||
} else {
|
||||
let workspace = project.as_ref().map(VirtualProject::workspace);
|
||||
let WorkspacePython {
|
||||
source,
|
||||
python_request,
|
||||
requires_python,
|
||||
} = WorkspacePython::from_request(
|
||||
python.as_deref().map(PythonRequest::parse),
|
||||
workspace,
|
||||
&groups,
|
||||
requires_python,
|
||||
&source,
|
||||
)?;
|
||||
}
|
||||
project_dir,
|
||||
no_config,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let reporter = PythonDownloadReporter::single(printer);
|
||||
let interpreter = PythonInstallation::find_or_download(
|
||||
python_request.as_ref(),
|
||||
EnvironmentPreference::Any,
|
||||
python_preference,
|
||||
python_downloads,
|
||||
&client_builder,
|
||||
cache,
|
||||
Some(&reporter),
|
||||
install_mirrors.python_install_mirror.as_deref(),
|
||||
install_mirrors.pypy_install_mirror.as_deref(),
|
||||
install_mirrors.python_downloads_json_url.as_deref(),
|
||||
)
|
||||
.await?
|
||||
.into_interpreter();
|
||||
|
||||
if let Some(requires_python) = requires_python.as_ref() {
|
||||
validate_project_requires_python(
|
||||
&interpreter,
|
||||
workspace,
|
||||
&groups,
|
||||
requires_python,
|
||||
&source,
|
||||
)?;
|
||||
}
|
||||
interpreter
|
||||
};
|
||||
|
||||
temp_dir = cache.venv_dir()?;
|
||||
Some(uv_virtualenv::create_venv(
|
||||
@@ -204,7 +228,139 @@ pub(crate) async fn check(
|
||||
|
||||
// Select an environment and, if we found a project, sync it before running checks.
|
||||
let mut workspace_metadata = None;
|
||||
let venv_path = if let Some(project) = &project {
|
||||
let venv_path = if let Some(script) = &script {
|
||||
let extras = extras.with_defaults(DefaultExtras::default());
|
||||
let venv = if let Some(venv) = isolated_venv {
|
||||
venv
|
||||
} else {
|
||||
ScriptEnvironment::get_or_init(
|
||||
script.into(),
|
||||
python.as_deref().map(PythonRequest::parse),
|
||||
&client_builder,
|
||||
python_preference,
|
||||
python_downloads,
|
||||
&install_mirrors,
|
||||
no_sync,
|
||||
no_config,
|
||||
Some(false),
|
||||
cache,
|
||||
DryRun::Disabled,
|
||||
printer,
|
||||
)
|
||||
.await?
|
||||
.into_environment()?
|
||||
};
|
||||
|
||||
let state = UniversalState::default();
|
||||
let lock_target = LockTarget::Script(script);
|
||||
// Scripts always run in an isolated environment, so `--no-sync` has no effect.
|
||||
let _environment_lock = venv
|
||||
.lock()
|
||||
.await
|
||||
.inspect_err(|err| {
|
||||
tracing::warn!("Failed to acquire environment lock: {err}");
|
||||
})
|
||||
.ok();
|
||||
let sync_state = state.fork();
|
||||
let mode = if let Some(frozen_source) = frozen {
|
||||
LockMode::Frozen(frozen_source.into())
|
||||
} else if let LockCheck::Enabled(lock_check) = lock_check {
|
||||
LockMode::Locked(venv.interpreter(), lock_check)
|
||||
} else if isolated || !lock_target.lock_path().is_file() {
|
||||
LockMode::DryRun(venv.interpreter())
|
||||
} else {
|
||||
LockMode::Write(venv.interpreter())
|
||||
};
|
||||
let result = match Box::pin(
|
||||
project::lock::LockOperation::new(
|
||||
mode,
|
||||
&settings.resolver,
|
||||
&client_builder,
|
||||
&state,
|
||||
Box::new(SummaryResolveLogger),
|
||||
&concurrency,
|
||||
cache,
|
||||
workspace_cache,
|
||||
printer,
|
||||
preview,
|
||||
)
|
||||
.execute(lock_target),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(result) => result,
|
||||
Err(ProjectError::Operation(err)) => {
|
||||
return diagnostics::OperationDiagnostic::with_system_certs(
|
||||
client_builder.system_certs(),
|
||||
)
|
||||
.report(err)
|
||||
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()));
|
||||
}
|
||||
Err(err) => return Err(err.into()),
|
||||
};
|
||||
|
||||
let target = InstallTarget::Script {
|
||||
script,
|
||||
lock: result.lock(),
|
||||
};
|
||||
match project::sync::do_sync(
|
||||
target,
|
||||
&venv,
|
||||
&extras,
|
||||
&groups,
|
||||
None,
|
||||
InstallOptions::default(),
|
||||
Modifications::Sufficient,
|
||||
None,
|
||||
(&settings).into(),
|
||||
&client_builder,
|
||||
&sync_state,
|
||||
Box::new(SummaryInstallLogger),
|
||||
installer_metadata,
|
||||
&concurrency,
|
||||
cache,
|
||||
workspace_cache,
|
||||
DryRun::Disabled,
|
||||
printer,
|
||||
preview,
|
||||
&malware_settings,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {}
|
||||
Err(ProjectError::Operation(err)) => {
|
||||
return diagnostics::OperationDiagnostic::with_system_certs(
|
||||
client_builder.system_certs(),
|
||||
)
|
||||
.report(err)
|
||||
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()));
|
||||
}
|
||||
Err(err) => return Err(err.into()),
|
||||
}
|
||||
|
||||
if no_sync {
|
||||
warn_user!(
|
||||
"`--no-sync` is a no-op for Python scripts with inline metadata, which always run in isolation"
|
||||
);
|
||||
}
|
||||
|
||||
let lock = result.into_lock();
|
||||
let metadata = crate::commands::workspace::metadata::metadata_from_target(
|
||||
Some(&venv),
|
||||
InstallTarget::Script {
|
||||
script,
|
||||
lock: &lock,
|
||||
},
|
||||
&extras,
|
||||
&groups,
|
||||
&settings.resolver,
|
||||
)?;
|
||||
let mut metadata = metadata.to_json()?;
|
||||
metadata.push('\n');
|
||||
workspace_metadata = Some(metadata);
|
||||
|
||||
Some(venv.root().to_owned())
|
||||
} else if let Some(project) = &project {
|
||||
let extras = extras.with_defaults(DefaultExtras::default());
|
||||
|
||||
let venv = if let Some(venv) = isolated_venv {
|
||||
@@ -384,7 +540,6 @@ pub(crate) async fn check(
|
||||
},
|
||||
};
|
||||
let metadata = crate::commands::workspace::metadata::metadata_from_target(
|
||||
project.workspace(),
|
||||
(!no_sync).then_some(&venv),
|
||||
target,
|
||||
&extras,
|
||||
@@ -410,6 +565,7 @@ pub(crate) async fn check(
|
||||
ty_version,
|
||||
ty_path,
|
||||
&target_dir,
|
||||
script.as_ref().map(|script| script.path.as_path()),
|
||||
venv_path.as_deref(),
|
||||
workspace_metadata,
|
||||
exclude_newer,
|
||||
|
||||
@@ -40,6 +40,7 @@ pub(super) async fn run(
|
||||
version: Option<String>,
|
||||
ty_path: Option<PathBuf>,
|
||||
target_dir: &Path,
|
||||
check_target: Option<&Path>,
|
||||
venv_path: Option<&Path>,
|
||||
workspace_metadata: Option<String>,
|
||||
exclude_newer: Option<jiff::Timestamp>,
|
||||
@@ -145,6 +146,16 @@ pub(super) async fn run(
|
||||
let mut command = Command::new(&ty_path);
|
||||
command.current_dir(target_dir);
|
||||
command.arg("check");
|
||||
if let Some(check_target) = check_target {
|
||||
// Check only the requested script. Keep the path relative to the working directory for
|
||||
// stable diagnostics, and use `--` so option-like filenames are treated as paths.
|
||||
command.arg("--");
|
||||
command.arg(
|
||||
check_target
|
||||
.strip_prefix(target_dir)
|
||||
.unwrap_or(check_target),
|
||||
);
|
||||
}
|
||||
// Opt into ty querying uv for project metadata.
|
||||
command.env("TY_UV", "1");
|
||||
|
||||
|
||||
@@ -867,7 +867,7 @@ impl ScriptInterpreter {
|
||||
}
|
||||
|
||||
/// Consume the [`PythonInstallation`] and return the [`Interpreter`].
|
||||
fn into_interpreter(self) -> Interpreter {
|
||||
pub(crate) fn into_interpreter(self) -> Interpreter {
|
||||
match self {
|
||||
Self::Interpreter(interpreter) => interpreter,
|
||||
Self::Environment(venv) => venv.into_interpreter(),
|
||||
@@ -1609,7 +1609,7 @@ impl ProjectEnvironment {
|
||||
///
|
||||
/// Returns an error if the environment was created in `--dry-run` mode, as dropping the
|
||||
/// associated temporary directory could lead to errors downstream.
|
||||
fn into_environment(self) -> Result<PythonEnvironment, ProjectError> {
|
||||
pub(crate) fn into_environment(self) -> Result<PythonEnvironment, ProjectError> {
|
||||
match self {
|
||||
Self::Existing(environment) => Ok(environment),
|
||||
Self::Replaced(environment) => Ok(environment),
|
||||
@@ -1644,7 +1644,7 @@ impl std::ops::Deref for ProjectEnvironment {
|
||||
|
||||
/// The Python environment for a script.
|
||||
#[derive(Debug)]
|
||||
enum ScriptEnvironment {
|
||||
pub(crate) enum ScriptEnvironment {
|
||||
/// An existing [`PythonEnvironment`] was discovered, which satisfies the script's requirements.
|
||||
Existing(PythonEnvironment),
|
||||
/// An existing [`PythonEnvironment`] was discovered, but did not satisfy the script's
|
||||
@@ -1671,7 +1671,7 @@ enum ScriptEnvironment {
|
||||
|
||||
impl ScriptEnvironment {
|
||||
/// Initialize a virtual environment for a PEP 723 script.
|
||||
async fn get_or_init(
|
||||
pub(crate) async fn get_or_init(
|
||||
script: Pep723ItemRef<'_>,
|
||||
python_request: Option<PythonRequest>,
|
||||
client_builder: &BaseClientBuilder<'_>,
|
||||
@@ -1796,7 +1796,7 @@ impl ScriptEnvironment {
|
||||
///
|
||||
/// Returns an error if the environment was created in `--dry-run` mode, as dropping the
|
||||
/// associated temporary directory could lead to errors downstream.
|
||||
fn into_environment(self) -> Result<PythonEnvironment, ProjectError> {
|
||||
pub(crate) fn into_environment(self) -> Result<PythonEnvironment, ProjectError> {
|
||||
match self {
|
||||
Self::Existing(environment) => Ok(environment),
|
||||
Self::Replaced(environment) => Ok(environment),
|
||||
|
||||
@@ -11,17 +11,19 @@ use uv_configuration::{
|
||||
};
|
||||
use uv_preview::{Preview, PreviewFeature};
|
||||
use uv_python::{PythonDownloads, PythonEnvironment, PythonPreference, PythonRequest};
|
||||
use uv_resolver::{Installable, Metadata};
|
||||
use uv_resolver::Metadata;
|
||||
use uv_scripts::Pep723Script;
|
||||
use uv_settings::{MalwareCheckSettings, PythonInstallMirrors};
|
||||
use uv_warnings::warn_user;
|
||||
use uv_workspace::{DiscoveryOptions, VirtualProject, Workspace, WorkspaceCache};
|
||||
use uv_workspace::{DiscoveryOptions, VirtualProject, WorkspaceCache};
|
||||
|
||||
use crate::commands::pip::loggers::DefaultResolveLogger;
|
||||
use crate::commands::project::install_target::InstallTarget;
|
||||
use crate::commands::project::lock::{LockMode, LockOperation};
|
||||
use crate::commands::project::lock_target::LockTarget;
|
||||
use crate::commands::project::{
|
||||
ProjectEnvironment, ProjectError, ProjectInterpreter, UniversalState, WorkspacePython,
|
||||
ProjectEnvironment, ProjectError, ProjectInterpreter, ScriptEnvironment, ScriptInterpreter,
|
||||
UniversalState, WorkspacePython,
|
||||
};
|
||||
use crate::commands::{ExitStatus, diagnostics};
|
||||
use crate::printer::Printer;
|
||||
@@ -42,6 +44,7 @@ pub(crate) async fn metadata(
|
||||
malware_settings: MalwareCheckSettings,
|
||||
settings: ResolverSettings,
|
||||
client_builder: BaseClientBuilder<'_>,
|
||||
script: Option<Pep723Script>,
|
||||
python_preference: PythonPreference,
|
||||
python_downloads: PythonDownloads,
|
||||
concurrency: Concurrency,
|
||||
@@ -58,14 +61,19 @@ pub(crate) async fn metadata(
|
||||
);
|
||||
}
|
||||
|
||||
let virtual_project = VirtualProject::discover(
|
||||
project_dir,
|
||||
&DiscoveryOptions::default(),
|
||||
cache,
|
||||
workspace_cache,
|
||||
)
|
||||
.await?;
|
||||
let target = LockTarget::Workspace(virtual_project.workspace());
|
||||
let virtual_project;
|
||||
let target = if let Some(script) = script.as_ref() {
|
||||
LockTarget::Script(script)
|
||||
} else {
|
||||
virtual_project = VirtualProject::discover(
|
||||
project_dir,
|
||||
&DiscoveryOptions::default(),
|
||||
cache,
|
||||
workspace_cache,
|
||||
)
|
||||
.await?;
|
||||
LockTarget::Workspace(virtual_project.workspace())
|
||||
};
|
||||
|
||||
// Don't enable any groups' requires-python for interpreter discovery.
|
||||
let groups = DependencyGroupsWithDefaults::none();
|
||||
@@ -75,33 +83,54 @@ pub(crate) async fn metadata(
|
||||
let mode = if let Some(frozen_source) = frozen {
|
||||
LockMode::Frozen(frozen_source.into())
|
||||
} else {
|
||||
let workspace_python = WorkspacePython::from_request(
|
||||
python.as_deref().map(PythonRequest::parse),
|
||||
Some(virtual_project.workspace()),
|
||||
&groups,
|
||||
project_dir,
|
||||
no_config,
|
||||
)
|
||||
.await?;
|
||||
interpreter = ProjectInterpreter::discover(
|
||||
virtual_project.workspace(),
|
||||
&groups,
|
||||
workspace_python,
|
||||
&client_builder,
|
||||
python_preference,
|
||||
python_downloads,
|
||||
&install_mirrors,
|
||||
false,
|
||||
Some(false),
|
||||
cache,
|
||||
printer,
|
||||
)
|
||||
.await?
|
||||
.into_interpreter();
|
||||
interpreter = match target {
|
||||
LockTarget::Script(script) => ScriptInterpreter::discover(
|
||||
script.into(),
|
||||
python.as_deref().map(PythonRequest::parse),
|
||||
&client_builder,
|
||||
python_preference,
|
||||
python_downloads,
|
||||
&install_mirrors,
|
||||
false,
|
||||
no_config,
|
||||
Some(false),
|
||||
cache,
|
||||
printer,
|
||||
)
|
||||
.await?
|
||||
.into_interpreter(),
|
||||
LockTarget::Workspace(workspace) => {
|
||||
let workspace_python = WorkspacePython::from_request(
|
||||
python.as_deref().map(PythonRequest::parse),
|
||||
Some(workspace),
|
||||
&groups,
|
||||
project_dir,
|
||||
no_config,
|
||||
)
|
||||
.await?;
|
||||
ProjectInterpreter::discover(
|
||||
workspace,
|
||||
&groups,
|
||||
workspace_python,
|
||||
&client_builder,
|
||||
python_preference,
|
||||
python_downloads,
|
||||
&install_mirrors,
|
||||
false,
|
||||
Some(false),
|
||||
cache,
|
||||
printer,
|
||||
)
|
||||
.await?
|
||||
.into_interpreter()
|
||||
}
|
||||
};
|
||||
|
||||
if let LockCheck::Enabled(lock_check) = lock_check {
|
||||
LockMode::Locked(&interpreter, lock_check)
|
||||
} else if dry_run.enabled() {
|
||||
} else if dry_run.enabled()
|
||||
|| (matches!(target, LockTarget::Script(_)) && !target.lock_path().is_file())
|
||||
{
|
||||
LockMode::DryRun(&interpreter)
|
||||
} else {
|
||||
LockMode::Write(&interpreter)
|
||||
@@ -132,27 +161,62 @@ pub(crate) async fn metadata(
|
||||
{
|
||||
Ok(lock) => {
|
||||
let lock = lock.into_lock();
|
||||
let mut export = Metadata::from_lock(virtual_project.workspace(), &lock)?;
|
||||
let install_target = match target {
|
||||
LockTarget::Workspace(workspace) => InstallTarget::Workspace {
|
||||
workspace,
|
||||
lock: &lock,
|
||||
},
|
||||
LockTarget::Script(script) => InstallTarget::Script {
|
||||
script,
|
||||
lock: &lock,
|
||||
},
|
||||
};
|
||||
let mut export = metadata_for_target(install_target)?;
|
||||
if sync {
|
||||
let environment = ProjectEnvironment::get_or_init(
|
||||
virtual_project.workspace(),
|
||||
&groups,
|
||||
python.as_deref().map(PythonRequest::parse),
|
||||
&install_mirrors,
|
||||
&client_builder,
|
||||
python_preference,
|
||||
python_downloads,
|
||||
false,
|
||||
no_config,
|
||||
Some(false),
|
||||
cache,
|
||||
DryRun::Disabled,
|
||||
printer,
|
||||
)
|
||||
.await?;
|
||||
let environment = match target {
|
||||
LockTarget::Workspace(workspace) => ProjectEnvironment::get_or_init(
|
||||
workspace,
|
||||
&groups,
|
||||
python.as_deref().map(PythonRequest::parse),
|
||||
&install_mirrors,
|
||||
&client_builder,
|
||||
python_preference,
|
||||
python_downloads,
|
||||
false,
|
||||
no_config,
|
||||
Some(false),
|
||||
cache,
|
||||
DryRun::Disabled,
|
||||
printer,
|
||||
)
|
||||
.await?
|
||||
.into_environment()?,
|
||||
LockTarget::Script(script) => ScriptEnvironment::get_or_init(
|
||||
script.into(),
|
||||
python.as_deref().map(PythonRequest::parse),
|
||||
&client_builder,
|
||||
python_preference,
|
||||
python_downloads,
|
||||
&install_mirrors,
|
||||
false,
|
||||
no_config,
|
||||
Some(false),
|
||||
cache,
|
||||
DryRun::Disabled,
|
||||
printer,
|
||||
)
|
||||
.await?
|
||||
.into_environment()?,
|
||||
};
|
||||
let _lock = environment
|
||||
.lock()
|
||||
.await
|
||||
.inspect_err(|err| {
|
||||
tracing::warn!("Failed to acquire environment lock: {err}");
|
||||
})
|
||||
.ok();
|
||||
let module_owners = collect_module_owners(
|
||||
virtual_project.workspace(),
|
||||
&lock,
|
||||
install_target,
|
||||
&environment,
|
||||
&settings,
|
||||
&client_builder,
|
||||
@@ -185,16 +249,15 @@ pub(crate) async fn metadata(
|
||||
}
|
||||
}
|
||||
|
||||
/// Build workspace metadata from an existing lock and environment without synchronizing it.
|
||||
/// Build metadata from an existing lock and environment without synchronizing it.
|
||||
pub(crate) fn metadata_from_target(
|
||||
workspace: &Workspace,
|
||||
environment: Option<&PythonEnvironment>,
|
||||
target: InstallTarget<'_>,
|
||||
extras: &ExtrasSpecificationWithDefaults,
|
||||
groups: &DependencyGroupsWithDefaults,
|
||||
settings: &ResolverSettings,
|
||||
) -> Result<Metadata> {
|
||||
let mut export = Metadata::from_lock(workspace, target.lock())?;
|
||||
let mut export = metadata_for_target(target)?;
|
||||
if let Some(environment) = environment {
|
||||
let module_owners = find_module_owners(target, environment, extras, groups, settings)
|
||||
.context("Failed to collect module owners")?;
|
||||
@@ -206,6 +269,22 @@ pub(crate) fn metadata_from_target(
|
||||
Ok(export)
|
||||
}
|
||||
|
||||
fn metadata_for_target(target: InstallTarget<'_>) -> Result<Metadata> {
|
||||
match target {
|
||||
InstallTarget::Project {
|
||||
workspace, lock, ..
|
||||
}
|
||||
| InstallTarget::Projects {
|
||||
workspace, lock, ..
|
||||
}
|
||||
| InstallTarget::Workspace { workspace, lock }
|
||||
| InstallTarget::NonProjectWorkspace { workspace, lock } => {
|
||||
Ok(Metadata::from_lock(workspace, lock)?)
|
||||
}
|
||||
InstallTarget::Script { script, lock } => Ok(Metadata::from_script(&script.path, lock)?),
|
||||
}
|
||||
}
|
||||
|
||||
fn print_metadata(export: &Metadata, printer: Printer) -> Result<ExitStatus> {
|
||||
writeln!(printer.stdout(), "{}", export.to_json()?)?;
|
||||
|
||||
|
||||
@@ -14,9 +14,9 @@ use uv_normalize::{DefaultExtras, DefaultGroups, PackageName};
|
||||
use uv_preview::Preview;
|
||||
use uv_pypi_types::ModuleName;
|
||||
use uv_python::PythonEnvironment;
|
||||
use uv_resolver::{Installable, Lock, Metadata};
|
||||
use uv_resolver::{Installable, Metadata};
|
||||
use uv_settings::MalwareCheckSettings;
|
||||
use uv_workspace::{Workspace, WorkspaceCache};
|
||||
use uv_workspace::WorkspaceCache;
|
||||
|
||||
use crate::commands::pip::loggers::DefaultInstallLogger;
|
||||
use crate::commands::pip::operations::Modifications;
|
||||
@@ -33,8 +33,7 @@ use crate::settings::{InstallerSettingsRef, ResolverSettings};
|
||||
/// without removing unrelated packages from an existing environment. Only distributions in the
|
||||
/// selected resolution are assigned package IDs, so those unrelated packages are not reported.
|
||||
pub(crate) async fn collect_module_owners(
|
||||
workspace: &Workspace,
|
||||
lock: &Lock,
|
||||
target: InstallTarget<'_>,
|
||||
venv: &PythonEnvironment,
|
||||
settings: &ResolverSettings,
|
||||
client_builder: &BaseClientBuilder<'_>,
|
||||
@@ -45,9 +44,7 @@ pub(crate) async fn collect_module_owners(
|
||||
preview: Preview,
|
||||
malware_settings: &MalwareCheckSettings,
|
||||
) -> Result<BTreeMap<ModuleName, Vec<String>>> {
|
||||
let target = InstallTarget::Workspace { workspace, lock };
|
||||
let extras = ExtrasSpecification::from_all_extras().with_defaults(DefaultExtras::default());
|
||||
let groups = DependencyGroups::from_all_groups().with_defaults(DefaultGroups::default());
|
||||
let (extras, groups) = target_selection(target);
|
||||
let Some(package_ids) = selected_package_ids(target, venv, &extras, &groups, settings)? else {
|
||||
return Ok(BTreeMap::new());
|
||||
};
|
||||
@@ -170,6 +167,27 @@ fn find_module_owners_in_environment(
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn target_selection(
|
||||
target: InstallTarget<'_>,
|
||||
) -> (
|
||||
ExtrasSpecificationWithDefaults,
|
||||
DependencyGroupsWithDefaults,
|
||||
) {
|
||||
match target {
|
||||
InstallTarget::Script { .. } => (
|
||||
ExtrasSpecification::default().with_defaults(DefaultExtras::default()),
|
||||
DependencyGroups::default().with_defaults(DefaultGroups::default()),
|
||||
),
|
||||
InstallTarget::Project { .. }
|
||||
| InstallTarget::Projects { .. }
|
||||
| InstallTarget::Workspace { .. }
|
||||
| InstallTarget::NonProjectWorkspace { .. } => (
|
||||
ExtrasSpecification::from_all_extras().with_defaults(DefaultExtras::default()),
|
||||
DependencyGroups::from_all_groups().with_defaults(DefaultGroups::default()),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_virtual(dist: &ResolvedDist) -> bool {
|
||||
let ResolvedDist::Installable { dist, .. } = dist else {
|
||||
return false;
|
||||
|
||||
@@ -362,6 +362,10 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
|
||||
| ProjectCommand::Audit(uv_cli::AuditArgs {
|
||||
script: Some(script),
|
||||
..
|
||||
})
|
||||
| ProjectCommand::Check(uv_cli::CheckArgs {
|
||||
script: Some(script),
|
||||
..
|
||||
}) => match Pep723Script::read(script).await {
|
||||
Ok(Some(script)) => Some(Pep723Item::Script(script)),
|
||||
Ok(None) => {
|
||||
@@ -382,6 +386,29 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
} else if let Commands::Workspace(WorkspaceNamespace {
|
||||
command: WorkspaceCommand::Metadata(args),
|
||||
}) = &*cli.command
|
||||
&& let Some(script) = args.script.as_ref()
|
||||
{
|
||||
match Pep723Script::read(script).await {
|
||||
Ok(Some(script)) => Some(Pep723Item::Script(script)),
|
||||
Ok(None) => {
|
||||
bail!(
|
||||
"`{}` does not contain a PEP 723 metadata tag; run `{}` to initialize the script",
|
||||
script.user_display().cyan(),
|
||||
format!("uv init --script {}", script.user_display()).green()
|
||||
)
|
||||
}
|
||||
Err(Pep723Error::Io(err)) if err.kind() == std::io::ErrorKind::NotFound => {
|
||||
bail!(
|
||||
"Failed to read `{}` (not found); run `{}` to create a PEP 723 script",
|
||||
script.user_display().cyan(),
|
||||
format!("uv init --script {}", script.user_display()).green()
|
||||
)
|
||||
}
|
||||
Err(err) => return Err(err.into()),
|
||||
}
|
||||
} else if let Commands::Python(uv_cli::PythonNamespace {
|
||||
command:
|
||||
PythonCommand::Find(uv_cli::PythonFindArgs {
|
||||
@@ -1995,6 +2022,11 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
|
||||
.combine(Refresh::from(args.settings.upgrade.clone())),
|
||||
);
|
||||
|
||||
let script = script.and_then(|script| match script {
|
||||
Pep723Item::Script(script) => Some(script),
|
||||
Pep723Item::Remote(..) | Pep723Item::Stdin(..) => None,
|
||||
});
|
||||
|
||||
Box::pin(commands::metadata(
|
||||
&project_dir,
|
||||
args.lock_check,
|
||||
@@ -2007,6 +2039,7 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
|
||||
args.malware_settings,
|
||||
args.settings,
|
||||
client_builder.subcommand(vec!["workspace".to_owned(), "metadata".to_owned()]),
|
||||
script,
|
||||
globals.python_preference,
|
||||
globals.python_downloads,
|
||||
globals.concurrency,
|
||||
@@ -2783,6 +2816,11 @@ async fn run_project(
|
||||
.combine(Refresh::from(args.settings.resolver.upgrade.clone())),
|
||||
);
|
||||
|
||||
let script = script.and_then(|script| match script {
|
||||
Pep723Item::Script(script) => Some(script),
|
||||
Pep723Item::Remote(..) | Pep723Item::Stdin(..) => None,
|
||||
});
|
||||
|
||||
Box::pin(commands::check(
|
||||
project_dir,
|
||||
args.ty_path,
|
||||
@@ -2797,6 +2835,7 @@ async fn run_project(
|
||||
args.settings,
|
||||
args.ty_version,
|
||||
args.show_version,
|
||||
script,
|
||||
client_builder.subcommand(vec!["check".to_owned()]),
|
||||
globals.python_preference,
|
||||
globals.python_downloads,
|
||||
|
||||
@@ -2093,6 +2093,8 @@ impl UpgradeSettings {
|
||||
/// The resolved settings to use for a `lock` invocation.
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct MetadataSettings {
|
||||
#[expect(dead_code)]
|
||||
pub(crate) script: Option<PathBuf>,
|
||||
pub(crate) lock_check: LockCheck,
|
||||
pub(crate) frozen: Option<FrozenSource>,
|
||||
pub(crate) dry_run: DryRun,
|
||||
@@ -2112,6 +2114,7 @@ impl MetadataSettings {
|
||||
environment: EnvironmentOptions,
|
||||
) -> Self {
|
||||
let MetadataArgs {
|
||||
script,
|
||||
locked,
|
||||
frozen,
|
||||
dry_run,
|
||||
@@ -2137,6 +2140,7 @@ impl MetadataSettings {
|
||||
let malware_settings = MalwareCheckSettings::from(&environment);
|
||||
|
||||
Self {
|
||||
script,
|
||||
lock_check: resolve_lock_check(locked),
|
||||
frozen: resolve_frozen(frozen),
|
||||
dry_run: DryRun::from_args(dry_run),
|
||||
@@ -2652,7 +2656,7 @@ pub(crate) struct TreeSettings {
|
||||
pub(crate) invert: bool,
|
||||
pub(crate) outdated: bool,
|
||||
pub(crate) show_sizes: bool,
|
||||
#[allow(dead_code)]
|
||||
#[expect(dead_code)]
|
||||
pub(crate) script: Option<PathBuf>,
|
||||
pub(crate) python_version: Option<PythonVersion>,
|
||||
pub(crate) python_platform: Option<TargetTriple>,
|
||||
@@ -2975,6 +2979,8 @@ impl FormatSettings {
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct CheckSettings {
|
||||
pub(crate) ty_path: Option<PathBuf>,
|
||||
#[expect(dead_code)]
|
||||
pub(crate) script: Option<PathBuf>,
|
||||
pub(crate) extras: ExtrasSpecification,
|
||||
pub(crate) groups: DependencyGroups,
|
||||
pub(crate) lock_check: LockCheck,
|
||||
@@ -2999,6 +3005,7 @@ impl CheckSettings {
|
||||
environment: EnvironmentOptions,
|
||||
) -> Self {
|
||||
let CheckArgs {
|
||||
script,
|
||||
extra,
|
||||
all_extras,
|
||||
no_extra,
|
||||
@@ -3052,6 +3059,7 @@ impl CheckSettings {
|
||||
|
||||
Self {
|
||||
ty_path: environment.ty_path,
|
||||
script,
|
||||
extras: ExtrasSpecification::from_args(
|
||||
extra.unwrap_or_default(),
|
||||
no_extra,
|
||||
|
||||
@@ -559,6 +559,53 @@ fn check_uses_ty_from_environment() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "test-pypi")]
|
||||
fn check_script() -> Result<()> {
|
||||
let context =
|
||||
uv_test::test_context!("3.12").with_filter((r"WARN Failed to fetch `ty`[^\n]*\n", ""));
|
||||
|
||||
// If `ty` accidentally uses the workspace environment, it will see this incompatible stub
|
||||
// instead of the script dependency and report that `IniConfig` is not callable.
|
||||
let workspace_iniconfig = context.site_packages().join("iniconfig");
|
||||
fs_err::create_dir_all(&workspace_iniconfig)?;
|
||||
fs_err::write(workspace_iniconfig.join("__init__.pyi"), "IniConfig: int\n")?;
|
||||
|
||||
let script = context.temp_dir.child("-script.py");
|
||||
script.write_str(indoc! {r#"
|
||||
# /// script
|
||||
# requires-python = ">=3.12"
|
||||
# dependencies = ["iniconfig"]
|
||||
# ///
|
||||
|
||||
import iniconfig
|
||||
|
||||
iniconfig.IniConfig("config.ini")
|
||||
"#})?;
|
||||
context
|
||||
.temp_dir
|
||||
.child("unrelated.py")
|
||||
.write_str(indoc! {r#"
|
||||
value: int = "wrong"
|
||||
"#})?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.check().arg("--script").arg(script.path()).arg("--no-sync"), @"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
All checks passed!
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv check` is experimental and may change without warning. Pass `--preview-features check-command` to disable this warning.
|
||||
Installed 1 package in [TIME]
|
||||
warning: `--no-sync` is a no-op for Python scripts with inline metadata, which always run in isolation
|
||||
");
|
||||
|
||||
assert!(!context.temp_dir.child("-script.py.lock").exists());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_passes_workspace_metadata_to_ty() -> Result<()> {
|
||||
let context = uv_test::test_context!("3.12");
|
||||
@@ -648,6 +695,53 @@ fn check_no_sync_errors_on_invalid_lockfile() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_script_no_sync_errors_on_invalid_lockfile() -> Result<()> {
|
||||
let context = uv_test::test_context!("3.12");
|
||||
|
||||
let script = context.temp_dir.child("script.py");
|
||||
script.write_str(indoc! {r#"
|
||||
# /// script
|
||||
# requires-python = ">=3.12"
|
||||
# dependencies = []
|
||||
# ///
|
||||
|
||||
x: int = 1
|
||||
"#})?;
|
||||
context
|
||||
.temp_dir
|
||||
.child("script.py.lock")
|
||||
.write_str("invalid")?;
|
||||
|
||||
uv_snapshot!(
|
||||
context.filters(),
|
||||
context
|
||||
.check()
|
||||
.arg("--script")
|
||||
.arg(script.path())
|
||||
.arg("--no-sync")
|
||||
.arg("--ty-version")
|
||||
.arg("0.0.17")
|
||||
.env(EnvVars::RUST_LOG, "error"),
|
||||
@"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv check` is experimental and may change without warning. Pass `--preview-features check-command` to disable this warning.
|
||||
error: Failed to parse `uv.lock`
|
||||
Caused by: TOML parse error at line 1, column 8
|
||||
|
|
||||
1 | invalid
|
||||
| ^
|
||||
key with no value, expected `=`
|
||||
"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_rejects_tool_arguments() {
|
||||
let context = uv_test::test_context_with_versions!(&[]);
|
||||
|
||||
@@ -15,6 +15,17 @@ fn write_wheel(
|
||||
name: &str,
|
||||
dist_info_prefix: &str,
|
||||
files: &[(&str, &str)],
|
||||
) -> Result<()> {
|
||||
write_wheel_with_metadata(path, name, "0.1.0", dist_info_prefix, "", files)
|
||||
}
|
||||
|
||||
fn write_wheel_with_metadata(
|
||||
path: &Path,
|
||||
name: &str,
|
||||
version: &str,
|
||||
dist_info_prefix: &str,
|
||||
additional_metadata: &str,
|
||||
files: &[(&str, &str)],
|
||||
) -> Result<()> {
|
||||
let mut writer = ZipFileWriter::new(Vec::new());
|
||||
let mut record = Vec::new();
|
||||
@@ -27,10 +38,15 @@ fn write_wheel(
|
||||
|
||||
let metadata_path = format!("{dist_info_prefix}.dist-info/METADATA");
|
||||
let entry = ZipEntryBuilder::new(metadata_path.clone().into(), Compression::Stored);
|
||||
block_on(writer.write_entry_whole(
|
||||
entry,
|
||||
format!("Metadata-Version: 2.1\nName: {name}\nVersion: 0.1.0\n").as_bytes(),
|
||||
))?;
|
||||
block_on(
|
||||
writer.write_entry_whole(
|
||||
entry,
|
||||
format!(
|
||||
"Metadata-Version: 2.1\nName: {name}\nVersion: {version}\n{additional_metadata}"
|
||||
)
|
||||
.as_bytes(),
|
||||
),
|
||||
)?;
|
||||
record.push(format!("{metadata_path},,"));
|
||||
|
||||
let wheel_path = format!("{dist_info_prefix}.dist-info/WHEEL");
|
||||
@@ -102,6 +118,303 @@ fn workspace_metadata_simple() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "test-pypi")]
|
||||
fn workspace_metadata_script() -> Result<()> {
|
||||
let context = uv_test::test_context!("3.12");
|
||||
let script = context.temp_dir.child("script.py");
|
||||
script.write_str(
|
||||
r#"# /// script
|
||||
# requires-python = ">=3.12"
|
||||
# dependencies = ["iniconfig"]
|
||||
# ///
|
||||
|
||||
import iniconfig
|
||||
"#,
|
||||
)?;
|
||||
|
||||
uv_snapshot!(
|
||||
context.filters(),
|
||||
context
|
||||
.workspace_metadata()
|
||||
.arg("--script")
|
||||
.arg(script.path())
|
||||
.arg("--sync"),
|
||||
@r#"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
{
|
||||
"schema": {
|
||||
"version": "preview"
|
||||
},
|
||||
"workspace_root": "[TEMP_DIR]/",
|
||||
"environment": {
|
||||
"root": "[CACHE_DIR]/environments-v2/script-[HASH]"
|
||||
},
|
||||
"script": {
|
||||
"path": "[TEMP_DIR]/script.py",
|
||||
"id": "script+[TEMP_DIR]/script.py"
|
||||
},
|
||||
"requires_python": ">=3.12",
|
||||
"conflicts": {
|
||||
"sets": []
|
||||
},
|
||||
"module_owners": {
|
||||
"iniconfig": [
|
||||
{
|
||||
"package_id": "iniconfig==2.0.0@registry+https://pypi.org/simple"
|
||||
}
|
||||
],
|
||||
"iniconfig._parse": [
|
||||
{
|
||||
"package_id": "iniconfig==2.0.0@registry+https://pypi.org/simple"
|
||||
}
|
||||
],
|
||||
"iniconfig._version": [
|
||||
{
|
||||
"package_id": "iniconfig==2.0.0@registry+https://pypi.org/simple"
|
||||
}
|
||||
],
|
||||
"iniconfig.exceptions": [
|
||||
{
|
||||
"package_id": "iniconfig==2.0.0@registry+https://pypi.org/simple"
|
||||
}
|
||||
]
|
||||
},
|
||||
"resolution": {
|
||||
"iniconfig==2.0.0@registry+https://pypi.org/simple": {
|
||||
"name": "iniconfig",
|
||||
"version": "2.0.0",
|
||||
"source": {
|
||||
"registry": {
|
||||
"url": "https://pypi.org/simple"
|
||||
}
|
||||
},
|
||||
"kind": "package",
|
||||
"dependencies": [],
|
||||
"sdist": {
|
||||
"url": "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz",
|
||||
"hashes": {
|
||||
"sha256": "2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"
|
||||
},
|
||||
"size": 4646,
|
||||
"upload_time": "2023-01-07T11:08:11.254Z"
|
||||
},
|
||||
"wheels": [
|
||||
{
|
||||
"url": "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl",
|
||||
"hashes": {
|
||||
"sha256": "b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"
|
||||
},
|
||||
"size": 5892,
|
||||
"upload_time": "2023-01-07T11:08:09.864Z",
|
||||
"filename": "iniconfig-2.0.0-py3-none-any.whl"
|
||||
}
|
||||
]
|
||||
},
|
||||
"script+[TEMP_DIR]/script.py": {
|
||||
"kind": "script",
|
||||
"path": "[TEMP_DIR]/script.py",
|
||||
"dependencies": [
|
||||
{
|
||||
"id": "iniconfig==2.0.0@registry+https://pypi.org/simple"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
----- stderr -----
|
||||
warning: The `uv workspace metadata` command is experimental and may change without warning. Pass `--preview-features workspace-metadata` to disable this warning.
|
||||
Resolved 1 package in [TIME]
|
||||
"#
|
||||
);
|
||||
|
||||
assert!(!context.temp_dir.child("script.py.lock").exists());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workspace_metadata_script_no_dependencies() -> Result<()> {
|
||||
let context = uv_test::test_context!("3.12");
|
||||
let script = context.temp_dir.child("script.py");
|
||||
script.write_str(
|
||||
r#"# /// script
|
||||
# requires-python = ">=3.12"
|
||||
# dependencies = []
|
||||
# ///
|
||||
|
||||
print("Hello, world!")
|
||||
"#,
|
||||
)?;
|
||||
|
||||
uv_snapshot!(
|
||||
context.filters(),
|
||||
context
|
||||
.workspace_metadata()
|
||||
.arg("--script")
|
||||
.arg(script.path()),
|
||||
@r#"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
{
|
||||
"schema": {
|
||||
"version": "preview"
|
||||
},
|
||||
"workspace_root": "[TEMP_DIR]/",
|
||||
"script": {
|
||||
"path": "[TEMP_DIR]/script.py",
|
||||
"id": "script+[TEMP_DIR]/script.py"
|
||||
},
|
||||
"requires_python": ">=3.12",
|
||||
"conflicts": {
|
||||
"sets": []
|
||||
},
|
||||
"resolution": {
|
||||
"script+[TEMP_DIR]/script.py": {
|
||||
"kind": "script",
|
||||
"path": "[TEMP_DIR]/script.py",
|
||||
"dependencies": []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
----- stderr -----
|
||||
warning: The `uv workspace metadata` command is experimental and may change without warning. Pass `--preview-features workspace-metadata` to disable this warning.
|
||||
Resolved in [TIME]
|
||||
"#
|
||||
);
|
||||
|
||||
assert!(!context.temp_dir.child("script.py.lock").exists());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workspace_metadata_script_dependency_edges() -> Result<()> {
|
||||
let context = uv_test::test_context!("3.12");
|
||||
|
||||
let child = context
|
||||
.temp_dir
|
||||
.child("metadata_edge_child-0.1.0-py3-none-any.whl");
|
||||
write_wheel(
|
||||
child.path(),
|
||||
"metadata-edge-child",
|
||||
"metadata_edge_child-0.1.0",
|
||||
&[],
|
||||
)?;
|
||||
let child_url = Url::from_file_path(child.path())
|
||||
.map_err(|()| anyhow::anyhow!("failed to convert wheel path to file URL"))?;
|
||||
|
||||
let first = context
|
||||
.temp_dir
|
||||
.child("metadata_edge-1.0.0-py3-none-any.whl");
|
||||
write_wheel_with_metadata(
|
||||
first.path(),
|
||||
"metadata-edge",
|
||||
"1.0.0",
|
||||
"metadata_edge-1.0.0",
|
||||
&format!(
|
||||
"Provides-Extra: feature\nRequires-Dist: metadata-edge-child @ {child_url}; extra == 'feature'\n"
|
||||
),
|
||||
&[],
|
||||
)?;
|
||||
let first_url = Url::from_file_path(first.path())
|
||||
.map_err(|()| anyhow::anyhow!("failed to convert wheel path to file URL"))?;
|
||||
|
||||
let second = context
|
||||
.temp_dir
|
||||
.child("metadata_edge-2.0.0-py3-none-any.whl");
|
||||
write_wheel_with_metadata(
|
||||
second.path(),
|
||||
"metadata-edge",
|
||||
"2.0.0",
|
||||
"metadata_edge-2.0.0",
|
||||
&format!(
|
||||
"Provides-Extra: feature\nRequires-Dist: metadata-edge-child @ {child_url}; extra == 'feature'\n"
|
||||
),
|
||||
&[],
|
||||
)?;
|
||||
let second_url = Url::from_file_path(second.path())
|
||||
.map_err(|()| anyhow::anyhow!("failed to convert wheel path to file URL"))?;
|
||||
|
||||
let script = context.temp_dir.child("script.py");
|
||||
script.write_str(&format!(
|
||||
r#"# /// script
|
||||
# requires-python = ">=3.12"
|
||||
# dependencies = [
|
||||
# "metadata-edge[feature] @ {first_url}; sys_platform == 'win32'",
|
||||
# "metadata-edge[feature] @ {second_url}; sys_platform != 'win32'",
|
||||
# ]
|
||||
# ///
|
||||
"#
|
||||
))?;
|
||||
|
||||
let assert = context
|
||||
.workspace_metadata()
|
||||
.arg("--script")
|
||||
.arg(script.path())
|
||||
.assert()
|
||||
.success();
|
||||
let metadata: serde_json::Value = serde_json::from_slice(&assert.get_output().stdout)?;
|
||||
|
||||
let resolution = metadata["resolution"]
|
||||
.as_object()
|
||||
.ok_or_else(|| anyhow::anyhow!("metadata resolution was not an object"))?;
|
||||
let script_id = metadata["script"]["id"]
|
||||
.as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("script ID was not a string"))?;
|
||||
let script_node = resolution
|
||||
.get(script_id)
|
||||
.ok_or_else(|| anyhow::anyhow!("missing resolution node for {script_id}"))?;
|
||||
|
||||
insta::with_settings!({ filters => context.filters() }, {
|
||||
insta::assert_json_snapshot!(serde_json::json!({
|
||||
"script": metadata["script"],
|
||||
"node": script_node,
|
||||
}), @r#"
|
||||
{
|
||||
"node": {
|
||||
"dependencies": [
|
||||
{
|
||||
"id": "metadata-edge[feature]==2.0.0@path+[TEMP_DIR]/metadata_edge-2.0.0-py3-none-any.whl",
|
||||
"marker": "sys_platform != 'win32'"
|
||||
},
|
||||
{
|
||||
"id": "metadata-edge[feature]==1.0.0@path+[TEMP_DIR]/metadata_edge-1.0.0-py3-none-any.whl",
|
||||
"marker": "sys_platform == 'win32'"
|
||||
}
|
||||
],
|
||||
"kind": "script",
|
||||
"path": "[TEMP_DIR]/script.py"
|
||||
},
|
||||
"script": {
|
||||
"id": "script+[TEMP_DIR]/script.py",
|
||||
"path": "[TEMP_DIR]/script.py"
|
||||
}
|
||||
}
|
||||
"#);
|
||||
});
|
||||
|
||||
for dependency in script_node["dependencies"]
|
||||
.as_array()
|
||||
.ok_or_else(|| anyhow::anyhow!("script dependencies was not an array"))?
|
||||
{
|
||||
let id = dependency["id"]
|
||||
.as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("script dependency ID was not a string"))?;
|
||||
anyhow::ensure!(
|
||||
resolution.contains_key(id),
|
||||
"missing resolution node for {id}"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workspace_metadata_module_owners_from_locked_wheels() -> Result<()> {
|
||||
let context = uv_test::test_context!("3.12");
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
# Workspace metadata
|
||||
|
||||
`uv workspace metadata` exports the information uv has about your workspace as JSON so other tools
|
||||
can use it. In particular, if you want access to the information in a `uv.lock`, you should prefer
|
||||
this command's output, as `uv.lock` is not a stable format we guarantee anything about.
|
||||
`uv workspace metadata` exports the information uv has about your workspace or PEP 723 script as
|
||||
JSON so other tools can use it. In particular, if you want access to the information in a `uv.lock`
|
||||
or script lockfile, you should prefer this command's output, as lockfiles are not a stable format we
|
||||
guarantee anything about. Pass `--script path/to/script.py` to request metadata for a script.
|
||||
|
||||
The primary structure is the "resolution" field which contains the dependency graph with exact
|
||||
package versions that a `uv.lock` encodes.
|
||||
@@ -14,10 +15,12 @@ for the node it refers to, and an optional `marker` that
|
||||
[specifies on what platforms the dependency is required](https://packaging.python.org/en/latest/specifications/dependency-specifiers/#dependency-specifiers)
|
||||
(if there is no marker the dependency is always required).
|
||||
|
||||
Nodes in the graph are uniquely identified by package `name`, `version`, `source`, and `kind`.
|
||||
Package-derived nodes in the graph are uniquely identified by package `name`, `version`, `source`,
|
||||
and `kind`. Script nodes are identified by their path. All node ids should be treated as opaque.
|
||||
|
||||
There are 3 kinds of node in the graph:
|
||||
There are 4 kinds of node in the graph:
|
||||
|
||||
- `"script"` -- a PEP 723 script and its direct dependencies
|
||||
- `"package"` -- the package itself
|
||||
- `{ "extra": "extraname" }` -- an extra the package defines
|
||||
- `{ "group": "groupname" }` -- a dependency group the package defines
|
||||
@@ -63,15 +66,15 @@ the graph and get actual resolutions you will likely need to consult `conflicts`
|
||||
understand how to resolve `markers` for a specific platform.
|
||||
|
||||
The best way to avoid mistakes when working with multiple versions of a package is to keep your
|
||||
queries into the dependency graph rooted in operations on workspace members, as those are the
|
||||
natural entry-points to the graph that uv wants to work on, and can give coherent responses for:
|
||||
"install `member1` and `member2[extra]`".
|
||||
queries into the dependency graph rooted in operations on workspace members or the requested script.
|
||||
These are the natural entry-points to the graph and can give coherent responses for operations such
|
||||
as "install `member1` and `member2[extra]`" or "install this script's declared dependencies".
|
||||
|
||||
Another way to put this is that when possible _you should avoid iterating over the `resolution`
|
||||
object to find a node_. Only access `resolution` like a map using ids that were provided by another
|
||||
part of the metadata. The only ids this initially gives you access to are the ones listed in the
|
||||
`members` array, which lists all the workspace members. From there you may find the ids of that
|
||||
package's dependencies, extras, and dependency groups and recursively discover other packages.
|
||||
part of the metadata. For a workspace, the initial ids are listed in the `members` array. For a
|
||||
script, the initial id is `script.id`. From there you may recursively discover other packages by
|
||||
following dependency edges.
|
||||
|
||||
So rather than trying to find a node for anyio in the dependency graph directly, you should decide
|
||||
what workspace member(s) you're interested in analyzing as if they were going to be installed. While
|
||||
@@ -102,6 +105,13 @@ for member_name in ["package1", "package2"]:
|
||||
visit(metadata, to_analyze)
|
||||
```
|
||||
|
||||
For a script, start from its resolution node in exactly the same way:
|
||||
|
||||
```python
|
||||
script_node = metadata.resolution[metadata.script.id]
|
||||
visit(metadata, [script_node])
|
||||
```
|
||||
|
||||
Where `visit` is your favourite graph traversal algorithm like depth-first-search:
|
||||
|
||||
```python
|
||||
@@ -147,6 +157,14 @@ Here is a human-readable annotated example:
|
||||
// The absolute path to the environment root
|
||||
"root": "/workspace/.venv"
|
||||
},
|
||||
// Information about the script target, only present with `--script`.
|
||||
// Workspace metadata uses `members` below as its graph entry-points instead.
|
||||
"script": {
|
||||
// The absolute path to the script
|
||||
"path": "/workspace/script.py",
|
||||
// The id of the script's node in the `resolution` map below
|
||||
"id": "script+/workspace/script.py"
|
||||
},
|
||||
// Any requirements on the python version this workspace has
|
||||
//
|
||||
// `marker` fields all have this as an implicit constraint that is omitted for cleanliness
|
||||
@@ -193,8 +211,9 @@ Here is a human-readable annotated example:
|
||||
// Resolved information about packages and dependencies.
|
||||
//
|
||||
// Each entry in this map is a node in the dependency graph. There are currently
|
||||
// 3 kinds of node in the dependency graph, although more are planned in the future.
|
||||
// 4 kinds of node in the dependency graph, although more are planned in the future.
|
||||
//
|
||||
// * Scripts -- "kind": "script"
|
||||
// * Packages -- "kind": "package"
|
||||
// * Extras -- "kind": { "extra": "extraname" }
|
||||
// * Groups -- "kind": { "group": "groupname" }
|
||||
@@ -211,6 +230,18 @@ Here is a human-readable annotated example:
|
||||
// the same information in a more convenient form).
|
||||
"resolution": {
|
||||
|
||||
// The script node is present when metadata was requested with `--script`. Its dependencies
|
||||
// are the direct requirements declared by the script.
|
||||
"script+/workspace/script.py": {
|
||||
"kind": "script",
|
||||
"path": "/workspace/script.py",
|
||||
"dependencies": [
|
||||
{
|
||||
"id": "iniconfig==2.0.0@registry+https://pypi.org/simple"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
// This node is a workspace member
|
||||
"mypackage==0.1.0@editable+/workspace/packages/mypackage": {
|
||||
// The name of the package
|
||||
|
||||
Reference in New Issue
Block a user