mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-07 13:25:00 +00:00
Add PEP 723 support to uv run (#4656)
Closes #3096
## Summary
Enables `uv run foo.py` to execute PEP 723-compatible scripts.
For example, given:
```python
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "requests<3",
# "rich",
# ]
# ///
import requests
from rich.pretty import pprint
resp = requests.get("https://peps.python.org/api/peps.json")
data = resp.json()
pprint([(k, v["title"]) for k, v in data.items()][:10])
```

This commit is contained in:
parent
bbd2deb64f
commit
bfadadefaf
11 changed files with 552 additions and 39 deletions
21
Cargo.lock
generated
21
Cargo.lock
generated
|
@ -2074,9 +2074,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
version = "2.7.2"
|
version = "2.7.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d"
|
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memmap2"
|
name = "memmap2"
|
||||||
|
@ -4486,6 +4486,7 @@ dependencies = [
|
||||||
"uv-normalize",
|
"uv-normalize",
|
||||||
"uv-requirements",
|
"uv-requirements",
|
||||||
"uv-resolver",
|
"uv-resolver",
|
||||||
|
"uv-scripts",
|
||||||
"uv-settings",
|
"uv-settings",
|
||||||
"uv-tool",
|
"uv-tool",
|
||||||
"uv-toolchain",
|
"uv-toolchain",
|
||||||
|
@ -4979,6 +4980,22 @@ dependencies = [
|
||||||
"uv-warnings",
|
"uv-warnings",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "uv-scripts"
|
||||||
|
version = "0.0.1"
|
||||||
|
dependencies = [
|
||||||
|
"fs-err",
|
||||||
|
"indoc",
|
||||||
|
"memchr",
|
||||||
|
"once_cell",
|
||||||
|
"pep440_rs",
|
||||||
|
"pep508_rs",
|
||||||
|
"pypi-types",
|
||||||
|
"serde",
|
||||||
|
"thiserror",
|
||||||
|
"toml",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uv-settings"
|
name = "uv-settings"
|
||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
|
|
|
@ -43,6 +43,7 @@ uv-macros = { path = "crates/uv-macros" }
|
||||||
uv-normalize = { path = "crates/uv-normalize" }
|
uv-normalize = { path = "crates/uv-normalize" }
|
||||||
uv-requirements = { path = "crates/uv-requirements" }
|
uv-requirements = { path = "crates/uv-requirements" }
|
||||||
uv-resolver = { path = "crates/uv-resolver" }
|
uv-resolver = { path = "crates/uv-resolver" }
|
||||||
|
uv-scripts = { path = "crates/uv-scripts" }
|
||||||
uv-settings = { path = "crates/uv-settings" }
|
uv-settings = { path = "crates/uv-settings" }
|
||||||
uv-state = { path = "crates/uv-state" }
|
uv-state = { path = "crates/uv-state" }
|
||||||
uv-tool = { path = "crates/uv-tool" }
|
uv-tool = { path = "crates/uv-tool" }
|
||||||
|
@ -94,6 +95,7 @@ itertools = { version = "0.13.0" }
|
||||||
junction = { version = "1.0.0" }
|
junction = { version = "1.0.0" }
|
||||||
mailparse = { version = "0.15.0" }
|
mailparse = { version = "0.15.0" }
|
||||||
md-5 = { version = "0.10.6" }
|
md-5 = { version = "0.10.6" }
|
||||||
|
memchr = { version = "2.7.4" }
|
||||||
miette = { version = "7.2.0" }
|
miette = { version = "7.2.0" }
|
||||||
nanoid = { version = "0.4.0" }
|
nanoid = { version = "0.4.0" }
|
||||||
once_cell = { version = "1.19.0" }
|
once_cell = { version = "1.19.0" }
|
||||||
|
|
|
@ -321,4 +321,15 @@ impl RequirementsSpecification {
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
Self::from_sources(requirements, &[], &[], client_builder).await
|
Self::from_sources(requirements, &[], &[], client_builder).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Initialize a [`RequirementsSpecification`] from a list of [`Requirement`].
|
||||||
|
pub fn from_requirements(requirements: Vec<Requirement>) -> Self {
|
||||||
|
Self {
|
||||||
|
requirements: requirements
|
||||||
|
.into_iter()
|
||||||
|
.map(UnresolvedRequirementSpecification::from)
|
||||||
|
.collect(),
|
||||||
|
..Self::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
23
crates/uv-scripts/Cargo.toml
Normal file
23
crates/uv-scripts/Cargo.toml
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
[package]
|
||||||
|
name = "uv-scripts"
|
||||||
|
version = "0.0.1"
|
||||||
|
edition = "2021"
|
||||||
|
description = "Parse PEP 723-style Python scripts."
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
pep440_rs = { workspace = true }
|
||||||
|
pep508_rs = { workspace = true }
|
||||||
|
pypi-types = { workspace = true }
|
||||||
|
|
||||||
|
fs-err = { workspace = true, features = ["tokio"] }
|
||||||
|
memchr = { workspace = true }
|
||||||
|
once_cell = { workspace = true }
|
||||||
|
serde = { workspace = true, features = ["derive"] }
|
||||||
|
thiserror = { workspace = true }
|
||||||
|
toml = { workspace = true }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
indoc = { workspace = true }
|
283
crates/uv-scripts/src/lib.rs
Normal file
283
crates/uv-scripts/src/lib.rs
Normal file
|
@ -0,0 +1,283 @@
|
||||||
|
use std::io;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use memchr::memmem::Finder;
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use pypi_types::VerbatimParsedUrl;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
static FINDER: Lazy<Finder> = Lazy::new(|| Finder::new(b"# /// script"));
|
||||||
|
|
||||||
|
/// PEP 723 metadata as parsed from a `script` comment block.
|
||||||
|
///
|
||||||
|
/// See: <https://peps.python.org/pep-0723/>
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub struct Pep723Metadata {
|
||||||
|
pub dependencies: Vec<pep508_rs::Requirement<VerbatimParsedUrl>>,
|
||||||
|
pub requires_python: Option<pep440_rs::VersionSpecifiers>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum Pep723Error {
|
||||||
|
#[error(transparent)]
|
||||||
|
Io(#[from] io::Error),
|
||||||
|
#[error(transparent)]
|
||||||
|
Utf8(#[from] std::str::Utf8Error),
|
||||||
|
#[error(transparent)]
|
||||||
|
Toml(#[from] toml::de::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read the PEP 723 `script` metadata from a Python file, if it exists.
|
||||||
|
///
|
||||||
|
/// See: <https://peps.python.org/pep-0723/>
|
||||||
|
pub async fn read_pep723_metadata(
|
||||||
|
file: impl AsRef<Path>,
|
||||||
|
) -> Result<Option<Pep723Metadata>, 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))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Given the contents of a Python file, extract the `script` metadata block, with leading comment
|
||||||
|
/// hashes removed.
|
||||||
|
///
|
||||||
|
/// See: <https://peps.python.org/pep-0723/>
|
||||||
|
fn extract_script_tag(contents: &[u8]) -> Result<Option<String>, Pep723Error> {
|
||||||
|
// Identify the opening pragma.
|
||||||
|
let Some(index) = FINDER.find(contents) else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
// The opening pragma must be the first line, or immediately preceded by a newline.
|
||||||
|
if !(index == 0 || matches!(contents[index - 1], b'\r' | b'\n')) {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode as UTF-8.
|
||||||
|
let contents = &contents[index..];
|
||||||
|
let contents = std::str::from_utf8(contents)?;
|
||||||
|
|
||||||
|
let mut lines = contents.lines();
|
||||||
|
|
||||||
|
// Ensure that the first line is exactly `# /// script`.
|
||||||
|
if !lines.next().is_some_and(|line| line == "# /// script") {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
// > Every line between these two lines (# /// TYPE and # ///) MUST be a comment starting
|
||||||
|
// > with #. If there are characters after the # then the first character MUST be a space. The
|
||||||
|
// > embedded content is formed by taking away the first two characters of each line if the
|
||||||
|
// > second character is a space, otherwise just the first character (which means the line
|
||||||
|
// > consists of only a single #).
|
||||||
|
let mut toml = vec![];
|
||||||
|
for line in lines {
|
||||||
|
// Remove the leading `#`.
|
||||||
|
let Some(line) = line.strip_prefix('#') else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
|
||||||
|
// If the line is empty, continue.
|
||||||
|
if line.is_empty() {
|
||||||
|
toml.push("");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, the line _must_ start with ` `.
|
||||||
|
let Some(line) = line.strip_prefix(' ') else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
toml.push(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the closing `# ///`. The precedence is such that we need to identify the _last_ such
|
||||||
|
// line.
|
||||||
|
//
|
||||||
|
// For example, given:
|
||||||
|
// ```python
|
||||||
|
// # /// script
|
||||||
|
// #
|
||||||
|
// # ///
|
||||||
|
// #
|
||||||
|
// # ///
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// The latter `///` is the closing pragma
|
||||||
|
let Some(index) = toml.iter().rev().position(|line| *line == "///") else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
let index = toml.len() - index;
|
||||||
|
|
||||||
|
// Discard any lines after the closing `# ///`.
|
||||||
|
//
|
||||||
|
// For example, given:
|
||||||
|
// ```python
|
||||||
|
// # /// script
|
||||||
|
// #
|
||||||
|
// # ///
|
||||||
|
// #
|
||||||
|
// #
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// We need to discard the last two lines.
|
||||||
|
toml.truncate(index - 1);
|
||||||
|
|
||||||
|
// Join the lines into a single string.
|
||||||
|
let toml = toml.join("\n") + "\n";
|
||||||
|
|
||||||
|
Ok(Some(toml))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
#[test]
|
||||||
|
fn missing_space() {
|
||||||
|
let contents = indoc::indoc! {r"
|
||||||
|
# /// script
|
||||||
|
#requires-python = '>=3.11'
|
||||||
|
# ///
|
||||||
|
"};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
super::extract_script_tag(contents.as_bytes()).unwrap(),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_closing_pragma() {
|
||||||
|
let contents = indoc::indoc! {r"
|
||||||
|
# /// script
|
||||||
|
# requires-python = '>=3.11'
|
||||||
|
# dependencies = [
|
||||||
|
# 'requests<3',
|
||||||
|
# 'rich',
|
||||||
|
# ]
|
||||||
|
"};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
super::extract_script_tag(contents.as_bytes()).unwrap(),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn leading_content() {
|
||||||
|
let contents = indoc::indoc! {r"
|
||||||
|
pass # /// script
|
||||||
|
# requires-python = '>=3.11'
|
||||||
|
# dependencies = [
|
||||||
|
# 'requests<3',
|
||||||
|
# 'rich',
|
||||||
|
# ]
|
||||||
|
# ///
|
||||||
|
#
|
||||||
|
#
|
||||||
|
"};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
super::extract_script_tag(contents.as_bytes()).unwrap(),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn simple() {
|
||||||
|
let contents = indoc::indoc! {r"
|
||||||
|
# /// script
|
||||||
|
# requires-python = '>=3.11'
|
||||||
|
# dependencies = [
|
||||||
|
# 'requests<3',
|
||||||
|
# 'rich',
|
||||||
|
# ]
|
||||||
|
# ///
|
||||||
|
"};
|
||||||
|
|
||||||
|
let expected = indoc::indoc! {r"
|
||||||
|
requires-python = '>=3.11'
|
||||||
|
dependencies = [
|
||||||
|
'requests<3',
|
||||||
|
'rich',
|
||||||
|
]
|
||||||
|
"};
|
||||||
|
|
||||||
|
let actual = super::extract_script_tag(contents.as_bytes())
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(actual, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn embedded_comment() {
|
||||||
|
let contents = indoc::indoc! {r"
|
||||||
|
# /// script
|
||||||
|
# embedded-csharp = '''
|
||||||
|
# /// <summary>
|
||||||
|
# /// text
|
||||||
|
# ///
|
||||||
|
# /// </summary>
|
||||||
|
# public class MyClass { }
|
||||||
|
# '''
|
||||||
|
# ///
|
||||||
|
"};
|
||||||
|
|
||||||
|
let expected = indoc::indoc! {r"
|
||||||
|
embedded-csharp = '''
|
||||||
|
/// <summary>
|
||||||
|
/// text
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public class MyClass { }
|
||||||
|
'''
|
||||||
|
"};
|
||||||
|
|
||||||
|
let actual = super::extract_script_tag(contents.as_bytes())
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(actual, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn trailing_lines() {
|
||||||
|
let contents = indoc::indoc! {r"
|
||||||
|
# /// script
|
||||||
|
# requires-python = '>=3.11'
|
||||||
|
# dependencies = [
|
||||||
|
# 'requests<3',
|
||||||
|
# 'rich',
|
||||||
|
# ]
|
||||||
|
# ///
|
||||||
|
#
|
||||||
|
#
|
||||||
|
"};
|
||||||
|
|
||||||
|
let expected = indoc::indoc! {r"
|
||||||
|
requires-python = '>=3.11'
|
||||||
|
dependencies = [
|
||||||
|
'requests<3',
|
||||||
|
'rich',
|
||||||
|
]
|
||||||
|
"};
|
||||||
|
|
||||||
|
let actual = super::extract_script_tag(contents.as_bytes())
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(actual, expected);
|
||||||
|
}
|
||||||
|
}
|
|
@ -33,9 +33,10 @@ uv-installer = { workspace = true }
|
||||||
uv-normalize = { workspace = true }
|
uv-normalize = { workspace = true }
|
||||||
uv-requirements = { workspace = true }
|
uv-requirements = { workspace = true }
|
||||||
uv-resolver = { workspace = true }
|
uv-resolver = { workspace = true }
|
||||||
|
uv-scripts = { workspace = true }
|
||||||
uv-settings = { workspace = true, features = ["schemars"] }
|
uv-settings = { workspace = true, features = ["schemars"] }
|
||||||
uv-toolchain = { workspace = true, features = ["schemars"]}
|
|
||||||
uv-tool = { workspace = true }
|
uv-tool = { workspace = true }
|
||||||
|
uv-toolchain = { workspace = true, features = ["schemars"]}
|
||||||
uv-types = { workspace = true }
|
uv-types = { workspace = true }
|
||||||
uv-virtualenv = { workspace = true }
|
uv-virtualenv = { workspace = true }
|
||||||
uv-warnings = { workspace = true }
|
uv-warnings = { workspace = true }
|
||||||
|
|
|
@ -15,7 +15,7 @@ use uv_distribution::Workspace;
|
||||||
use uv_fs::Simplified;
|
use uv_fs::Simplified;
|
||||||
use uv_git::GitResolver;
|
use uv_git::GitResolver;
|
||||||
use uv_installer::{SatisfiesResult, SitePackages};
|
use uv_installer::{SatisfiesResult, SitePackages};
|
||||||
use uv_requirements::{RequirementsSource, RequirementsSpecification};
|
use uv_requirements::RequirementsSpecification;
|
||||||
use uv_resolver::{FlatIndex, InMemoryIndex, OptionsBuilder, PythonRequirement, RequiresPython};
|
use uv_resolver::{FlatIndex, InMemoryIndex, OptionsBuilder, PythonRequirement, RequiresPython};
|
||||||
use uv_toolchain::{
|
use uv_toolchain::{
|
||||||
request_from_version_file, EnvironmentPreference, Interpreter, PythonEnvironment, Toolchain,
|
request_from_version_file, EnvironmentPreference, Interpreter, PythonEnvironment, Toolchain,
|
||||||
|
@ -280,7 +280,7 @@ pub(crate) async fn init_environment(
|
||||||
/// Update a [`PythonEnvironment`] to satisfy a set of [`RequirementsSource`]s.
|
/// Update a [`PythonEnvironment`] to satisfy a set of [`RequirementsSource`]s.
|
||||||
pub(crate) async fn update_environment(
|
pub(crate) async fn update_environment(
|
||||||
venv: PythonEnvironment,
|
venv: PythonEnvironment,
|
||||||
requirements: &[RequirementsSource],
|
spec: RequirementsSpecification,
|
||||||
settings: &ResolverInstallerSettings,
|
settings: &ResolverInstallerSettings,
|
||||||
preview: PreviewMode,
|
preview: PreviewMode,
|
||||||
connectivity: Connectivity,
|
connectivity: Connectivity,
|
||||||
|
@ -305,17 +305,6 @@ pub(crate) async fn update_environment(
|
||||||
build_options,
|
build_options,
|
||||||
} = settings;
|
} = settings;
|
||||||
|
|
||||||
let client_builder = BaseClientBuilder::new()
|
|
||||||
.connectivity(connectivity)
|
|
||||||
.native_tls(native_tls)
|
|
||||||
.keyring(*keyring_provider);
|
|
||||||
|
|
||||||
// Read all requirements from the provided sources.
|
|
||||||
// TODO(zanieb): Consider allowing constraints and extras
|
|
||||||
// TODO(zanieb): Allow specifying extras somehow
|
|
||||||
let spec =
|
|
||||||
RequirementsSpecification::from_sources(requirements, &[], &[], &client_builder).await?;
|
|
||||||
|
|
||||||
// Check if the current environment satisfies the requirements
|
// Check if the current environment satisfies the requirements
|
||||||
let site_packages = SitePackages::from_environment(&venv)?;
|
let site_packages = SitePackages::from_environment(&venv)?;
|
||||||
if spec.source_trees.is_empty() && reinstall.is_none() {
|
if spec.source_trees.is_empty() && reinstall.is_none() {
|
||||||
|
|
|
@ -6,16 +6,17 @@ use anyhow::{Context, Result};
|
||||||
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::{VirtualProject, Workspace, WorkspaceError};
|
use uv_distribution::{VirtualProject, Workspace, WorkspaceError};
|
||||||
use uv_normalize::PackageName;
|
use uv_normalize::PackageName;
|
||||||
use uv_requirements::RequirementsSource;
|
use uv_requirements::{RequirementsSource, RequirementsSpecification};
|
||||||
use uv_toolchain::{
|
use uv_toolchain::{
|
||||||
EnvironmentPreference, Interpreter, PythonEnvironment, Toolchain, ToolchainPreference,
|
request_from_version_file, EnvironmentPreference, Interpreter, PythonEnvironment, Toolchain,
|
||||||
ToolchainRequest,
|
ToolchainPreference, ToolchainRequest, VersionRequest,
|
||||||
};
|
};
|
||||||
use uv_warnings::warn_user_once;
|
use uv_warnings::warn_user_once;
|
||||||
|
|
||||||
|
@ -49,8 +50,83 @@ pub(crate) async fn run(
|
||||||
// Parse the input command.
|
// Parse the input command.
|
||||||
let command = RunCommand::from(command);
|
let command = RunCommand::from(command);
|
||||||
|
|
||||||
|
// Determine whether the command to execute is a PEP 723 script.
|
||||||
|
let temp_dir;
|
||||||
|
let script_interpreter = if let RunCommand::Python(target, _) = &command {
|
||||||
|
if let Some(metadata) = uv_scripts::read_pep723_metadata(&target).await? {
|
||||||
|
debug!("Found PEP 723 script at: {}", target.display());
|
||||||
|
|
||||||
|
let spec = RequirementsSpecification::from_requirements(
|
||||||
|
metadata
|
||||||
|
.dependencies
|
||||||
|
.into_iter()
|
||||||
|
.map(Requirement::from)
|
||||||
|
.collect(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// (1) Explicit request from user
|
||||||
|
let python_request = if let Some(request) = python.as_deref() {
|
||||||
|
Some(ToolchainRequest::parse(request))
|
||||||
|
// (2) Request from `.python-version`
|
||||||
|
} else if let Some(request) = request_from_version_file().await? {
|
||||||
|
Some(request)
|
||||||
|
// (3) `Requires-Python` in `pyproject.toml`
|
||||||
|
} else {
|
||||||
|
metadata.requires_python.map(|requires_python| {
|
||||||
|
ToolchainRequest::Version(VersionRequest::Range(requires_python))
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let client_builder = BaseClientBuilder::new()
|
||||||
|
.connectivity(connectivity)
|
||||||
|
.native_tls(native_tls);
|
||||||
|
|
||||||
|
let interpreter = Toolchain::find_or_fetch(
|
||||||
|
python_request,
|
||||||
|
EnvironmentPreference::Any,
|
||||||
|
toolchain_preference,
|
||||||
|
client_builder,
|
||||||
|
cache,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.into_interpreter();
|
||||||
|
|
||||||
|
// Create a virtual environment
|
||||||
|
temp_dir = cache.environment()?;
|
||||||
|
let venv = uv_virtualenv::create_venv(
|
||||||
|
temp_dir.path(),
|
||||||
|
interpreter,
|
||||||
|
uv_virtualenv::Prompt::None,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Install the script requirements.
|
||||||
|
let environment = project::update_environment(
|
||||||
|
venv,
|
||||||
|
spec,
|
||||||
|
&settings,
|
||||||
|
preview,
|
||||||
|
connectivity,
|
||||||
|
concurrency,
|
||||||
|
native_tls,
|
||||||
|
cache,
|
||||||
|
printer,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Some(environment.into_interpreter())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
// Discover and sync the base environment.
|
// Discover and sync the base environment.
|
||||||
let base_interpreter = if isolated {
|
let base_interpreter = if let Some(script_interpreter) = script_interpreter {
|
||||||
|
Some(script_interpreter)
|
||||||
|
} else if isolated {
|
||||||
// package is `None`, isolated and package are marked as conflicting in clap.
|
// package is `None`, isolated and package are marked as conflicting in clap.
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
|
@ -199,11 +275,18 @@ pub(crate) async fn run(
|
||||||
false,
|
false,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
let client_builder = BaseClientBuilder::new()
|
||||||
|
.connectivity(connectivity)
|
||||||
|
.native_tls(native_tls);
|
||||||
|
|
||||||
|
let spec =
|
||||||
|
RequirementsSpecification::from_simple_sources(&requirements, &client_builder).await?;
|
||||||
|
|
||||||
// Install the ephemeral requirements.
|
// Install the ephemeral requirements.
|
||||||
Some(
|
Some(
|
||||||
project::update_environment(
|
project::update_environment(
|
||||||
venv,
|
venv,
|
||||||
&requirements,
|
spec,
|
||||||
&settings,
|
&settings,
|
||||||
preview,
|
preview,
|
||||||
connectivity,
|
connectivity,
|
||||||
|
@ -316,8 +399,8 @@ impl std::fmt::Display for RunCommand {
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Self::External(command, args) => {
|
Self::External(executable, args) => {
|
||||||
write!(f, "{}", command.to_string_lossy())?;
|
write!(f, "{}", executable.to_string_lossy())?;
|
||||||
for arg in args {
|
for arg in args {
|
||||||
write!(f, " {}", arg.to_string_lossy())?;
|
write!(f, " {}", arg.to_string_lossy())?;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,6 @@ use anyhow::{bail, Context, Result};
|
||||||
use distribution_types::Name;
|
use distribution_types::Name;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
|
|
||||||
use pep508_rs::Requirement;
|
|
||||||
use pypi_types::VerbatimParsedUrl;
|
use pypi_types::VerbatimParsedUrl;
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
use uv_cache::Cache;
|
use uv_cache::Cache;
|
||||||
|
@ -17,7 +16,7 @@ use uv_configuration::{Concurrency, PreviewMode, Reinstall};
|
||||||
use uv_fs::replace_symlink;
|
use uv_fs::replace_symlink;
|
||||||
use uv_fs::Simplified;
|
use uv_fs::Simplified;
|
||||||
use uv_installer::SitePackages;
|
use uv_installer::SitePackages;
|
||||||
use uv_requirements::RequirementsSource;
|
use uv_requirements::RequirementsSpecification;
|
||||||
use uv_tool::{entrypoint_paths, find_executable_directory, InstalledTools, Tool, ToolEntrypoint};
|
use uv_tool::{entrypoint_paths, find_executable_directory, InstalledTools, Tool, ToolEntrypoint};
|
||||||
use uv_toolchain::{EnvironmentPreference, Toolchain, ToolchainPreference, ToolchainRequest};
|
use uv_toolchain::{EnvironmentPreference, Toolchain, ToolchainPreference, ToolchainRequest};
|
||||||
use uv_warnings::warn_user_once;
|
use uv_warnings::warn_user_once;
|
||||||
|
@ -48,11 +47,12 @@ pub(crate) async fn install(
|
||||||
}
|
}
|
||||||
|
|
||||||
let from = if let Some(from) = from {
|
let from = if let Some(from) = from {
|
||||||
let from_requirement = Requirement::<VerbatimParsedUrl>::from_str(&from)?;
|
let from_requirement = pep508_rs::Requirement::<VerbatimParsedUrl>::from_str(&from)?;
|
||||||
// Check if the user provided more than just a name positionally or if that name conflicts with `--from`
|
// Check if the user provided more than just a name positionally or if that name conflicts with `--from`
|
||||||
if from_requirement.name.to_string() != package {
|
if from_requirement.name.to_string() != package {
|
||||||
// Determine if its an entirely different package or a conflicting specification
|
// Determine if its an entirely different package or a conflicting specification
|
||||||
let package_requirement = Requirement::<VerbatimParsedUrl>::from_str(&package)?;
|
let package_requirement =
|
||||||
|
pep508_rs::Requirement::<VerbatimParsedUrl>::from_str(&package)?;
|
||||||
if from_requirement.name == package_requirement.name {
|
if from_requirement.name == package_requirement.name {
|
||||||
bail!(
|
bail!(
|
||||||
"Package requirement `{}` provided with `--from` conflicts with install request `{}`",
|
"Package requirement `{}` provided with `--from` conflicts with install request `{}`",
|
||||||
|
@ -68,7 +68,7 @@ pub(crate) async fn install(
|
||||||
}
|
}
|
||||||
from_requirement
|
from_requirement
|
||||||
} else {
|
} else {
|
||||||
Requirement::<VerbatimParsedUrl>::from_str(&package)?
|
pep508_rs::Requirement::<VerbatimParsedUrl>::from_str(&package)?
|
||||||
};
|
};
|
||||||
|
|
||||||
let name = from.name.to_string();
|
let name = from.name.to_string();
|
||||||
|
@ -102,14 +102,19 @@ pub(crate) async fn install(
|
||||||
|
|
||||||
let requirements = [Ok(from.clone())]
|
let requirements = [Ok(from.clone())]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.chain(with.iter().map(|name| Requirement::from_str(name)))
|
.chain(
|
||||||
.collect::<Result<Vec<Requirement<VerbatimParsedUrl>>, _>>()?;
|
with.iter()
|
||||||
|
.map(|name| pep508_rs::Requirement::from_str(name)),
|
||||||
|
)
|
||||||
|
.collect::<Result<Vec<pep508_rs::Requirement<VerbatimParsedUrl>>, _>>()?;
|
||||||
|
|
||||||
// TODO(zanieb): Duplicative with the above parsing but needed for `update_environment`
|
let spec = RequirementsSpecification::from_requirements(
|
||||||
let requirements_sources = [RequirementsSource::from_package(from.to_string())]
|
requirements
|
||||||
.into_iter()
|
.iter()
|
||||||
.chain(with.into_iter().map(RequirementsSource::from_package))
|
.cloned()
|
||||||
.collect::<Vec<_>>();
|
.map(pypi_types::Requirement::from)
|
||||||
|
.collect(),
|
||||||
|
);
|
||||||
|
|
||||||
let Some(from) = requirements.first().cloned() else {
|
let Some(from) = requirements.first().cloned() else {
|
||||||
bail!("Expected at least one requirement")
|
bail!("Expected at least one requirement")
|
||||||
|
@ -139,7 +144,7 @@ pub(crate) async fn install(
|
||||||
// Install the ephemeral requirements.
|
// Install the ephemeral requirements.
|
||||||
let environment = update_environment(
|
let environment = update_environment(
|
||||||
environment,
|
environment,
|
||||||
&requirements_sources,
|
spec,
|
||||||
&settings,
|
&settings,
|
||||||
preview,
|
preview,
|
||||||
connectivity,
|
connectivity,
|
||||||
|
|
|
@ -11,9 +11,9 @@ use tracing::debug;
|
||||||
|
|
||||||
use uv_cache::Cache;
|
use uv_cache::Cache;
|
||||||
use uv_cli::ExternalCommand;
|
use uv_cli::ExternalCommand;
|
||||||
use uv_client::Connectivity;
|
use uv_client::{BaseClientBuilder, Connectivity};
|
||||||
use uv_configuration::{Concurrency, PreviewMode};
|
use uv_configuration::{Concurrency, PreviewMode};
|
||||||
use uv_requirements::RequirementsSource;
|
use uv_requirements::{RequirementsSource, RequirementsSpecification};
|
||||||
use uv_toolchain::{
|
use uv_toolchain::{
|
||||||
EnvironmentPreference, PythonEnvironment, Toolchain, ToolchainPreference, ToolchainRequest,
|
EnvironmentPreference, PythonEnvironment, Toolchain, ToolchainPreference, ToolchainRequest,
|
||||||
};
|
};
|
||||||
|
@ -60,6 +60,13 @@ pub(crate) async fn run(
|
||||||
.chain(with.into_iter().map(RequirementsSource::from_package))
|
.chain(with.into_iter().map(RequirementsSource::from_package))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let client_builder = BaseClientBuilder::new()
|
||||||
|
.connectivity(connectivity)
|
||||||
|
.native_tls(native_tls);
|
||||||
|
|
||||||
|
let spec =
|
||||||
|
RequirementsSpecification::from_simple_sources(&requirements, &client_builder).await?;
|
||||||
|
|
||||||
// TODO(zanieb): When implementing project-level tools, discover the project and check if it has the tool.
|
// TODO(zanieb): When implementing project-level tools, discover the project and check if it has the tool.
|
||||||
// TODO(zanieb): Determine if we should layer on top of the project environment if it is present.
|
// TODO(zanieb): Determine if we should layer on top of the project environment if it is present.
|
||||||
|
|
||||||
|
@ -93,7 +100,7 @@ pub(crate) async fn run(
|
||||||
let ephemeral_env = Some(
|
let ephemeral_env = Some(
|
||||||
update_environment(
|
update_environment(
|
||||||
venv,
|
venv,
|
||||||
&requirements,
|
spec,
|
||||||
&settings,
|
&settings,
|
||||||
preview,
|
preview,
|
||||||
connectivity,
|
connectivity,
|
||||||
|
|
|
@ -171,3 +171,95 @@ fn run_args() -> Result<()> {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Run a PEP 723-compatible script. The script should take precedence over the workspace
|
||||||
|
/// dependencies.
|
||||||
|
#[test]
|
||||||
|
fn run_script() -> Result<()> {
|
||||||
|
let context = TestContext::new("3.12");
|
||||||
|
|
||||||
|
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||||
|
pyproject_toml.write_str(indoc! { r#"
|
||||||
|
[project]
|
||||||
|
name = "foo"
|
||||||
|
version = "1.0.0"
|
||||||
|
requires-python = ">=3.8"
|
||||||
|
dependencies = ["anyio"]
|
||||||
|
"#
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// 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",
|
||||||
|
# ]
|
||||||
|
# ///
|
||||||
|
|
||||||
|
import iniconfig
|
||||||
|
"#
|
||||||
|
})?;
|
||||||
|
|
||||||
|
uv_snapshot!(context.filters(), context.run().arg("--preview").arg("main.py"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved 1 package in [TIME]
|
||||||
|
Prepared 1 package in [TIME]
|
||||||
|
Installed 1 package in [TIME]
|
||||||
|
+ iniconfig==2.0.0
|
||||||
|
"###);
|
||||||
|
|
||||||
|
// Otherwise, the script requirements should _not_ be available, but the project requirements
|
||||||
|
// should.
|
||||||
|
let test_non_script = context.temp_dir.child("main.py");
|
||||||
|
test_non_script.write_str(indoc! { r"
|
||||||
|
import iniconfig
|
||||||
|
"
|
||||||
|
})?;
|
||||||
|
|
||||||
|
uv_snapshot!(context.filters(), context.run().arg("--preview").arg("main.py"), @r###"
|
||||||
|
success: false
|
||||||
|
exit_code: 1
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved 6 packages in [TIME]
|
||||||
|
Prepared 4 packages in [TIME]
|
||||||
|
Installed 4 packages in [TIME]
|
||||||
|
+ anyio==4.3.0
|
||||||
|
+ foo==1.0.0 (from file://[TEMP_DIR]/)
|
||||||
|
+ idna==3.6
|
||||||
|
+ sniffio==1.3.1
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "[TEMP_DIR]/main.py", line 1, in <module>
|
||||||
|
import iniconfig
|
||||||
|
ModuleNotFoundError: No module named 'iniconfig'
|
||||||
|
"###);
|
||||||
|
|
||||||
|
// But the script should be runnable.
|
||||||
|
let test_non_script = context.temp_dir.child("main.py");
|
||||||
|
test_non_script.write_str(indoc! { r#"
|
||||||
|
import idna
|
||||||
|
|
||||||
|
print("Hello, world!")
|
||||||
|
"#
|
||||||
|
})?;
|
||||||
|
|
||||||
|
uv_snapshot!(context.filters(), context.run().arg("--preview").arg("main.py"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
Hello, world!
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved 6 packages in [TIME]
|
||||||
|
Audited 4 packages in [TIME]
|
||||||
|
"###);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue