mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-23 21:25:02 +00:00
Support PEP 723 scripts in uv add
and uv remove
(#5995)
## Summary Resolves https://github.com/astral-sh/uv/issues/4667 ## Test Plan `cargo test`
This commit is contained in:
parent
9b8c07bf18
commit
2d53e35e39
12 changed files with 1215 additions and 289 deletions
|
@ -2450,6 +2450,16 @@ pub struct AddArgs {
|
||||||
#[arg(long, conflicts_with = "isolated")]
|
#[arg(long, conflicts_with = "isolated")]
|
||||||
pub package: Option<PackageName>,
|
pub package: Option<PackageName>,
|
||||||
|
|
||||||
|
/// Add the dependency to the specified Python script, rather than to a project.
|
||||||
|
///
|
||||||
|
/// If provided, uv will add the dependency to the script's inline metadata
|
||||||
|
/// table, in adhere with PEP 723. If no such inline metadata table is present,
|
||||||
|
/// a new one will be created and added to the script. When executed via `uv run`,
|
||||||
|
/// uv will create a temporary environment for the script with all inline
|
||||||
|
/// dependencies installed.
|
||||||
|
#[arg(long, conflicts_with = "dev", conflicts_with = "optional")]
|
||||||
|
pub script: Option<PathBuf>,
|
||||||
|
|
||||||
/// The Python interpreter to use for resolving and syncing.
|
/// The Python interpreter to use for resolving and syncing.
|
||||||
///
|
///
|
||||||
/// See `uv help python` for details on Python discovery and supported
|
/// See `uv help python` for details on Python discovery and supported
|
||||||
|
@ -2509,6 +2519,13 @@ pub struct RemoveArgs {
|
||||||
#[arg(long, conflicts_with = "isolated")]
|
#[arg(long, conflicts_with = "isolated")]
|
||||||
pub package: Option<PackageName>,
|
pub package: Option<PackageName>,
|
||||||
|
|
||||||
|
/// Remove the dependency from the specified Python script, rather than from a project.
|
||||||
|
///
|
||||||
|
/// If provided, uv will remove the dependency from the script's inline metadata
|
||||||
|
/// table, in adhere with PEP 723.
|
||||||
|
#[arg(long)]
|
||||||
|
pub script: Option<PathBuf>,
|
||||||
|
|
||||||
/// The Python interpreter to use for resolving and syncing.
|
/// The Python interpreter to use for resolving and syncing.
|
||||||
///
|
///
|
||||||
/// See `uv help python` for details on Python discovery and supported
|
/// See `uv help python` for details on Python discovery and supported
|
||||||
|
|
|
@ -15,10 +15,8 @@ uv-settings = { workspace = true }
|
||||||
uv-workspace = { workspace = true }
|
uv-workspace = { workspace = true }
|
||||||
|
|
||||||
fs-err = { workspace = true, features = ["tokio"] }
|
fs-err = { workspace = true, features = ["tokio"] }
|
||||||
|
indoc = { workspace = true }
|
||||||
memchr = { workspace = true }
|
memchr = { workspace = true }
|
||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
toml = { workspace = true }
|
toml = { workspace = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
indoc = { workspace = true }
|
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::str::FromStr;
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
use memchr::memmem::Finder;
|
use memchr::memmem::Finder;
|
||||||
|
use pep440_rs::VersionSpecifiers;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
@ -17,8 +19,14 @@ static FINDER: LazyLock<Finder> = LazyLock::new(|| Finder::new(b"# /// script"))
|
||||||
/// A PEP 723 script, including its [`Pep723Metadata`].
|
/// A PEP 723 script, including its [`Pep723Metadata`].
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Pep723Script {
|
pub struct Pep723Script {
|
||||||
|
/// The path to the Python script.
|
||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
||||||
|
/// The parsed [`Pep723Metadata`] table from the script.
|
||||||
pub metadata: Pep723Metadata,
|
pub metadata: Pep723Metadata,
|
||||||
|
/// The content of the script after the metadata table.
|
||||||
|
pub raw: String,
|
||||||
|
/// The content of the script before the metadata table.
|
||||||
|
pub prelude: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Pep723Script {
|
impl Pep723Script {
|
||||||
|
@ -26,12 +34,76 @@ impl Pep723Script {
|
||||||
///
|
///
|
||||||
/// See: <https://peps.python.org/pep-0723/>
|
/// See: <https://peps.python.org/pep-0723/>
|
||||||
pub async fn read(file: impl AsRef<Path>) -> Result<Option<Self>, Pep723Error> {
|
pub async fn read(file: impl AsRef<Path>) -> Result<Option<Self>, Pep723Error> {
|
||||||
let metadata = Pep723Metadata::read(&file).await?;
|
let contents = match fs_err::tokio::read(&file).await {
|
||||||
Ok(metadata.map(|metadata| Self {
|
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(script_tag) = ScriptTag::parse(&contents)? else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parse the metadata.
|
||||||
|
let metadata = Pep723Metadata::from_str(&script_tag.metadata)?;
|
||||||
|
|
||||||
|
Ok(Some(Self {
|
||||||
path: file.as_ref().to_path_buf(),
|
path: file.as_ref().to_path_buf(),
|
||||||
metadata,
|
metadata,
|
||||||
|
raw: script_tag.script,
|
||||||
|
prelude: script_tag.prelude,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reads a Python script and generates a default PEP 723 metadata table.
|
||||||
|
///
|
||||||
|
/// See: <https://peps.python.org/pep-0723/>
|
||||||
|
pub async fn create(
|
||||||
|
file: impl AsRef<Path>,
|
||||||
|
requires_python: &VersionSpecifiers,
|
||||||
|
) -> Result<Self, Pep723Error> {
|
||||||
|
let contents = match fs_err::tokio::read(&file).await {
|
||||||
|
Ok(contents) => contents,
|
||||||
|
Err(err) => return Err(err.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Extract the `script` tag.
|
||||||
|
let default_metadata = indoc::formatdoc! {r#"
|
||||||
|
requires-python = "{requires_python}"
|
||||||
|
dependencies = []
|
||||||
|
"#,
|
||||||
|
requires_python = requires_python,
|
||||||
|
};
|
||||||
|
|
||||||
|
let (prelude, raw) = extract_shebang(&contents)?;
|
||||||
|
|
||||||
|
// Parse the metadata.
|
||||||
|
let metadata = Pep723Metadata::from_str(&default_metadata)?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
path: file.as_ref().to_path_buf(),
|
||||||
|
prelude: prelude.unwrap_or_default(),
|
||||||
|
metadata,
|
||||||
|
raw,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replace the existing metadata in the file with new metadata and write the updated content.
|
||||||
|
pub async fn write(&self, metadata: &str) -> Result<(), Pep723Error> {
|
||||||
|
let content = format!(
|
||||||
|
"{}{}{}",
|
||||||
|
if self.prelude.is_empty() {
|
||||||
|
String::new()
|
||||||
|
} else {
|
||||||
|
format!("{}\n", self.prelude)
|
||||||
|
},
|
||||||
|
serialize_metadata(metadata),
|
||||||
|
self.raw
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(fs_err::tokio::write(&self.path, content).await?)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// PEP 723 metadata as parsed from a `script` comment block.
|
/// PEP 723 metadata as parsed from a `script` comment block.
|
||||||
|
@ -41,30 +113,23 @@ impl Pep723Script {
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
pub struct Pep723Metadata {
|
pub struct Pep723Metadata {
|
||||||
pub dependencies: Option<Vec<pep508_rs::Requirement<VerbatimParsedUrl>>>,
|
pub dependencies: Option<Vec<pep508_rs::Requirement<VerbatimParsedUrl>>>,
|
||||||
pub requires_python: Option<pep440_rs::VersionSpecifiers>,
|
pub requires_python: Option<VersionSpecifiers>,
|
||||||
pub tool: Option<Tool>,
|
pub tool: Option<Tool>,
|
||||||
|
/// The raw unserialized document.
|
||||||
|
#[serde(skip)]
|
||||||
|
pub raw: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Pep723Metadata {
|
impl FromStr for Pep723Metadata {
|
||||||
/// Read the PEP 723 `script` metadata from a Python file, if it exists.
|
type Err = Pep723Error;
|
||||||
///
|
|
||||||
/// See: <https://peps.python.org/pep-0723/>
|
|
||||||
pub async fn read(file: impl AsRef<Path>) -> Result<Option<Self>, Pep723Error> {
|
|
||||||
let contents = match fs_err::tokio::read(file).await {
|
|
||||||
Ok(contents) => contents,
|
|
||||||
Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(None),
|
|
||||||
Err(err) => return Err(err.into()),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Extract the `script` tag.
|
/// Parse `Pep723Metadata` from a raw TOML string.
|
||||||
let Some(contents) = extract_script_tag(&contents)? else {
|
fn from_str(raw: &str) -> Result<Self, Self::Err> {
|
||||||
return Ok(None);
|
let metadata = toml::from_str(raw)?;
|
||||||
};
|
Ok(Pep723Metadata {
|
||||||
|
raw: raw.to_string(),
|
||||||
// Parse the metadata.
|
..metadata
|
||||||
let metadata = toml::from_str(&contents)?;
|
})
|
||||||
|
|
||||||
Ok(Some(metadata))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,34 +159,46 @@ pub enum Pep723Error {
|
||||||
Toml(#[from] toml::de::Error),
|
Toml(#[from] toml::de::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read the PEP 723 `script` metadata from a Python file, if it exists.
|
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||||
///
|
struct ScriptTag {
|
||||||
/// See: <https://peps.python.org/pep-0723/>
|
/// The content of the script before the metadata block.
|
||||||
pub async fn read_pep723_metadata(
|
prelude: String,
|
||||||
file: impl AsRef<Path>,
|
/// The metadata block.
|
||||||
) -> Result<Option<Pep723Metadata>, Pep723Error> {
|
metadata: String,
|
||||||
let contents = match fs_err::tokio::read(file).await {
|
/// The content of the script after the metadata block.
|
||||||
Ok(contents) => contents,
|
script: String,
|
||||||
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
|
impl ScriptTag {
|
||||||
/// hashes removed.
|
/// Given the contents of a Python file, extract the `script` metadata block with leading
|
||||||
|
/// comment hashes removed, any preceding shebang or content (prelude), and the remaining Python
|
||||||
|
/// script.
|
||||||
|
///
|
||||||
|
/// Given the following input string representing the contents of a Python script:
|
||||||
|
///
|
||||||
|
/// ```python
|
||||||
|
/// #!/usr/bin/env python3
|
||||||
|
/// # /// script
|
||||||
|
/// # requires-python = '>=3.11'
|
||||||
|
/// # dependencies = [
|
||||||
|
/// # 'requests<3',
|
||||||
|
/// # 'rich',
|
||||||
|
/// # ]
|
||||||
|
/// # ///
|
||||||
|
///
|
||||||
|
/// import requests
|
||||||
|
///
|
||||||
|
/// print("Hello, World!")
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// This function would return:
|
||||||
|
///
|
||||||
|
/// - Preamble: `#!/usr/bin/env python3\n`
|
||||||
|
/// - Metadata: `requires-python = '>=3.11'\ndependencies = [\n 'requests<3',\n 'rich',\n]`
|
||||||
|
/// - Script: `import requests\n\nprint("Hello, World!")\n`
|
||||||
///
|
///
|
||||||
/// See: <https://peps.python.org/pep-0723/>
|
/// See: <https://peps.python.org/pep-0723/>
|
||||||
fn extract_script_tag(contents: &[u8]) -> Result<Option<String>, Pep723Error> {
|
fn parse(contents: &[u8]) -> Result<Option<Self>, Pep723Error> {
|
||||||
// Identify the opening pragma.
|
// Identify the opening pragma.
|
||||||
let Some(index) = FINDER.find(contents) else {
|
let Some(index) = FINDER.find(contents) else {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
|
@ -132,6 +209,9 @@ fn extract_script_tag(contents: &[u8]) -> Result<Option<String>, Pep723Error> {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract the preceding content.
|
||||||
|
let prelude = std::str::from_utf8(&contents[..index])?;
|
||||||
|
|
||||||
// Decode as UTF-8.
|
// Decode as UTF-8.
|
||||||
let contents = &contents[index..];
|
let contents = &contents[index..];
|
||||||
let contents = std::str::from_utf8(contents)?;
|
let contents = std::str::from_utf8(contents)?;
|
||||||
|
@ -149,9 +229,15 @@ fn extract_script_tag(contents: &[u8]) -> Result<Option<String>, Pep723Error> {
|
||||||
// > second character is a space, otherwise just the first character (which means the line
|
// > second character is a space, otherwise just the first character (which means the line
|
||||||
// > consists of only a single #).
|
// > consists of only a single #).
|
||||||
let mut toml = vec![];
|
let mut toml = vec![];
|
||||||
for line in lines {
|
|
||||||
|
// Extract the content that follows the metadata block.
|
||||||
|
let mut python_script = vec![];
|
||||||
|
|
||||||
|
while let Some(line) = lines.next() {
|
||||||
// Remove the leading `#`.
|
// Remove the leading `#`.
|
||||||
let Some(line) = line.strip_prefix('#') else {
|
let Some(line) = line.strip_prefix('#') else {
|
||||||
|
python_script.push(line);
|
||||||
|
python_script.extend(lines);
|
||||||
break;
|
break;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -163,8 +249,11 @@ fn extract_script_tag(contents: &[u8]) -> Result<Option<String>, Pep723Error> {
|
||||||
|
|
||||||
// Otherwise, the line _must_ start with ` `.
|
// Otherwise, the line _must_ start with ` `.
|
||||||
let Some(line) = line.strip_prefix(' ') else {
|
let Some(line) = line.strip_prefix(' ') else {
|
||||||
|
python_script.push(line);
|
||||||
|
python_script.extend(lines);
|
||||||
break;
|
break;
|
||||||
};
|
};
|
||||||
|
|
||||||
toml.push(line);
|
toml.push(line);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -201,13 +290,62 @@ fn extract_script_tag(contents: &[u8]) -> Result<Option<String>, Pep723Error> {
|
||||||
toml.truncate(index - 1);
|
toml.truncate(index - 1);
|
||||||
|
|
||||||
// Join the lines into a single string.
|
// Join the lines into a single string.
|
||||||
let toml = toml.join("\n") + "\n";
|
let prelude = prelude.to_string();
|
||||||
|
let metadata = toml.join("\n") + "\n";
|
||||||
|
let script = python_script.join("\n") + "\n";
|
||||||
|
|
||||||
Ok(Some(toml))
|
Ok(Some(Self {
|
||||||
|
prelude,
|
||||||
|
metadata,
|
||||||
|
script,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extracts the shebang line from the given file contents and returns it along with the remaining
|
||||||
|
/// content.
|
||||||
|
fn extract_shebang(contents: &[u8]) -> Result<(Option<String>, String), Pep723Error> {
|
||||||
|
let contents = std::str::from_utf8(contents)?;
|
||||||
|
|
||||||
|
let mut lines = contents.lines();
|
||||||
|
|
||||||
|
// Check the first line for a shebang
|
||||||
|
if let Some(first_line) = lines.next() {
|
||||||
|
if first_line.starts_with("#!") {
|
||||||
|
let shebang = first_line.to_string();
|
||||||
|
let remaining_content: String = lines.collect::<Vec<&str>>().join("\n");
|
||||||
|
return Ok((Some(shebang), remaining_content));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((None, contents.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Formats the provided metadata by prefixing each line with `#` and wrapping it with script markers.
|
||||||
|
fn serialize_metadata(metadata: &str) -> String {
|
||||||
|
let mut output = String::with_capacity(metadata.len() + 2);
|
||||||
|
|
||||||
|
output.push_str("# /// script\n");
|
||||||
|
|
||||||
|
for line in metadata.lines() {
|
||||||
|
if line.is_empty() {
|
||||||
|
output.push('\n');
|
||||||
|
} else {
|
||||||
|
output.push_str("# ");
|
||||||
|
output.push_str(line);
|
||||||
|
output.push('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output.push_str("# ///\n");
|
||||||
|
|
||||||
|
output
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use crate::{serialize_metadata, ScriptTag};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn missing_space() {
|
fn missing_space() {
|
||||||
let contents = indoc::indoc! {r"
|
let contents = indoc::indoc! {r"
|
||||||
|
@ -216,10 +354,7 @@ mod tests {
|
||||||
# ///
|
# ///
|
||||||
"};
|
"};
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(ScriptTag::parse(contents.as_bytes()).unwrap(), None);
|
||||||
super::extract_script_tag(contents.as_bytes()).unwrap(),
|
|
||||||
None
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -233,10 +368,7 @@ mod tests {
|
||||||
# ]
|
# ]
|
||||||
"};
|
"};
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(ScriptTag::parse(contents.as_bytes()).unwrap(), None);
|
||||||
super::extract_script_tag(contents.as_bytes()).unwrap(),
|
|
||||||
None
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -253,10 +385,7 @@ mod tests {
|
||||||
#
|
#
|
||||||
"};
|
"};
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(ScriptTag::parse(contents.as_bytes()).unwrap(), None);
|
||||||
super::extract_script_tag(contents.as_bytes()).unwrap(),
|
|
||||||
None
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -269,9 +398,15 @@ mod tests {
|
||||||
# 'rich',
|
# 'rich',
|
||||||
# ]
|
# ]
|
||||||
# ///
|
# ///
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from rich.pretty import pprint
|
||||||
|
|
||||||
|
resp = requests.get('https://peps.python.org/api/peps.json')
|
||||||
|
data = resp.json()
|
||||||
"};
|
"};
|
||||||
|
|
||||||
let expected = indoc::indoc! {r"
|
let expected_metadata = indoc::indoc! {r"
|
||||||
requires-python = '>=3.11'
|
requires-python = '>=3.11'
|
||||||
dependencies = [
|
dependencies = [
|
||||||
'requests<3',
|
'requests<3',
|
||||||
|
@ -279,13 +414,64 @@ mod tests {
|
||||||
]
|
]
|
||||||
"};
|
"};
|
||||||
|
|
||||||
let actual = super::extract_script_tag(contents.as_bytes())
|
let expected_data = indoc::indoc! {r"
|
||||||
.unwrap()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(actual, expected);
|
import requests
|
||||||
|
from rich.pretty import pprint
|
||||||
|
|
||||||
|
resp = requests.get('https://peps.python.org/api/peps.json')
|
||||||
|
data = resp.json()
|
||||||
|
"};
|
||||||
|
|
||||||
|
let actual = ScriptTag::parse(contents.as_bytes()).unwrap().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(actual.prelude, String::new());
|
||||||
|
assert_eq!(actual.metadata, expected_metadata);
|
||||||
|
assert_eq!(actual.script, expected_data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn simple_with_shebang() {
|
||||||
|
let contents = indoc::indoc! {r"
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# /// 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()
|
||||||
|
"};
|
||||||
|
|
||||||
|
let expected_metadata = indoc::indoc! {r"
|
||||||
|
requires-python = '>=3.11'
|
||||||
|
dependencies = [
|
||||||
|
'requests<3',
|
||||||
|
'rich',
|
||||||
|
]
|
||||||
|
"};
|
||||||
|
|
||||||
|
let expected_data = indoc::indoc! {r"
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from rich.pretty import pprint
|
||||||
|
|
||||||
|
resp = requests.get('https://peps.python.org/api/peps.json')
|
||||||
|
data = resp.json()
|
||||||
|
"};
|
||||||
|
|
||||||
|
let actual = ScriptTag::parse(contents.as_bytes()).unwrap().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(actual.prelude, "#!/usr/bin/env python3\n".to_string());
|
||||||
|
assert_eq!(actual.metadata, expected_metadata);
|
||||||
|
assert_eq!(actual.script, expected_data);
|
||||||
|
}
|
||||||
#[test]
|
#[test]
|
||||||
fn embedded_comment() {
|
fn embedded_comment() {
|
||||||
let contents = indoc::indoc! {r"
|
let contents = indoc::indoc! {r"
|
||||||
|
@ -310,9 +496,10 @@ mod tests {
|
||||||
'''
|
'''
|
||||||
"};
|
"};
|
||||||
|
|
||||||
let actual = super::extract_script_tag(contents.as_bytes())
|
let actual = ScriptTag::parse(contents.as_bytes())
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.unwrap();
|
.unwrap()
|
||||||
|
.metadata;
|
||||||
|
|
||||||
assert_eq!(actual, expected);
|
assert_eq!(actual, expected);
|
||||||
}
|
}
|
||||||
|
@ -339,10 +526,44 @@ mod tests {
|
||||||
]
|
]
|
||||||
"};
|
"};
|
||||||
|
|
||||||
let actual = super::extract_script_tag(contents.as_bytes())
|
let actual = ScriptTag::parse(contents.as_bytes())
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.unwrap();
|
.unwrap()
|
||||||
|
.metadata;
|
||||||
|
|
||||||
assert_eq!(actual, expected);
|
assert_eq!(actual, expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serialize_metadata_formatting() {
|
||||||
|
let metadata = indoc::indoc! {r"
|
||||||
|
requires-python = '>=3.11'
|
||||||
|
dependencies = [
|
||||||
|
'requests<3',
|
||||||
|
'rich',
|
||||||
|
]
|
||||||
|
"};
|
||||||
|
|
||||||
|
let expected_output = indoc::indoc! {r"
|
||||||
|
# /// script
|
||||||
|
# requires-python = '>=3.11'
|
||||||
|
# dependencies = [
|
||||||
|
# 'requests<3',
|
||||||
|
# 'rich',
|
||||||
|
# ]
|
||||||
|
# ///
|
||||||
|
"};
|
||||||
|
|
||||||
|
let result = serialize_metadata(metadata);
|
||||||
|
assert_eq!(result, expected_output);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serialize_metadata_empty() {
|
||||||
|
let metadata = "";
|
||||||
|
let expected_output = "# /// script\n# ///\n";
|
||||||
|
|
||||||
|
let result = serialize_metadata(metadata);
|
||||||
|
assert_eq!(result, expected_output);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ use thiserror::Error;
|
||||||
use toml_edit::{Array, DocumentMut, Item, RawString, Table, TomlError, Value};
|
use toml_edit::{Array, DocumentMut, Item, RawString, Table, TomlError, Value};
|
||||||
use uv_fs::PortablePath;
|
use uv_fs::PortablePath;
|
||||||
|
|
||||||
use crate::pyproject::{DependencyType, PyProjectToml, Source};
|
use crate::pyproject::{DependencyType, Source};
|
||||||
|
|
||||||
/// Raw and mutable representation of a `pyproject.toml`.
|
/// Raw and mutable representation of a `pyproject.toml`.
|
||||||
///
|
///
|
||||||
|
@ -16,6 +16,7 @@ use crate::pyproject::{DependencyType, PyProjectToml, Source};
|
||||||
/// preserving comments and other structure, such as `uv add` and `uv remove`.
|
/// preserving comments and other structure, such as `uv add` and `uv remove`.
|
||||||
pub struct PyProjectTomlMut {
|
pub struct PyProjectTomlMut {
|
||||||
doc: DocumentMut,
|
doc: DocumentMut,
|
||||||
|
target: DependencyTarget,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
|
@ -47,11 +48,21 @@ pub enum ArrayEdit {
|
||||||
Add(usize),
|
Add(usize),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Specifies whether dependencies are added to a script file or a `pyproject.toml` file.
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||||
|
pub enum DependencyTarget {
|
||||||
|
/// A PEP 723 script, with inline metadata.
|
||||||
|
Script,
|
||||||
|
/// A project with a `pyproject.toml`.
|
||||||
|
PyProjectToml,
|
||||||
|
}
|
||||||
|
|
||||||
impl PyProjectTomlMut {
|
impl PyProjectTomlMut {
|
||||||
/// Initialize a [`PyProjectTomlMut`] from a [`PyProjectToml`].
|
/// Initialize a [`PyProjectTomlMut`] from a [`str`].
|
||||||
pub fn from_toml(pyproject: &PyProjectToml) -> Result<Self, Error> {
|
pub fn from_toml(raw: &str, target: DependencyTarget) -> Result<Self, Error> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
doc: pyproject.raw.parse().map_err(Box::new)?,
|
doc: raw.parse().map_err(Box::new)?,
|
||||||
|
target,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,6 +94,34 @@ impl PyProjectTomlMut {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Retrieves a mutable reference to the root [`Table`] of the TOML document, creating the
|
||||||
|
/// `project` table if necessary.
|
||||||
|
fn doc(&mut self) -> Result<&mut Table, Error> {
|
||||||
|
let doc = match self.target {
|
||||||
|
DependencyTarget::Script => self.doc.as_table_mut(),
|
||||||
|
DependencyTarget::PyProjectToml => self
|
||||||
|
.doc
|
||||||
|
.entry("project")
|
||||||
|
.or_insert(Item::Table(Table::new()))
|
||||||
|
.as_table_mut()
|
||||||
|
.ok_or(Error::MalformedDependencies)?,
|
||||||
|
};
|
||||||
|
Ok(doc)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieves an optional mutable reference to the `project` [`Table`], returning `None` if it
|
||||||
|
/// doesn't exist.
|
||||||
|
fn doc_mut(&mut self) -> Result<Option<&mut Table>, Error> {
|
||||||
|
let doc = match self.target {
|
||||||
|
DependencyTarget::Script => Some(self.doc.as_table_mut()),
|
||||||
|
DependencyTarget::PyProjectToml => self
|
||||||
|
.doc
|
||||||
|
.get_mut("project")
|
||||||
|
.map(|project| project.as_table_mut().ok_or(Error::MalformedSources))
|
||||||
|
.transpose()?,
|
||||||
|
};
|
||||||
|
Ok(doc)
|
||||||
|
}
|
||||||
/// Adds a dependency to `project.dependencies`.
|
/// Adds a dependency to `project.dependencies`.
|
||||||
///
|
///
|
||||||
/// Returns `true` if the dependency was added, `false` if it was updated.
|
/// Returns `true` if the dependency was added, `false` if it was updated.
|
||||||
|
@ -93,11 +132,7 @@ impl PyProjectTomlMut {
|
||||||
) -> Result<ArrayEdit, Error> {
|
) -> Result<ArrayEdit, Error> {
|
||||||
// Get or create `project.dependencies`.
|
// Get or create `project.dependencies`.
|
||||||
let dependencies = self
|
let dependencies = self
|
||||||
.doc
|
.doc()?
|
||||||
.entry("project")
|
|
||||||
.or_insert(Item::Table(Table::new()))
|
|
||||||
.as_table_mut()
|
|
||||||
.ok_or(Error::MalformedDependencies)?
|
|
||||||
.entry("dependencies")
|
.entry("dependencies")
|
||||||
.or_insert(Item::Value(Value::Array(Array::new())))
|
.or_insert(Item::Value(Value::Array(Array::new())))
|
||||||
.as_array_mut()
|
.as_array_mut()
|
||||||
|
@ -158,11 +193,7 @@ impl PyProjectTomlMut {
|
||||||
) -> Result<ArrayEdit, Error> {
|
) -> Result<ArrayEdit, Error> {
|
||||||
// Get or create `project.optional-dependencies`.
|
// Get or create `project.optional-dependencies`.
|
||||||
let optional_dependencies = self
|
let optional_dependencies = self
|
||||||
.doc
|
.doc()?
|
||||||
.entry("project")
|
|
||||||
.or_insert(Item::Table(Table::new()))
|
|
||||||
.as_table_mut()
|
|
||||||
.ok_or(Error::MalformedDependencies)?
|
|
||||||
.entry("optional-dependencies")
|
.entry("optional-dependencies")
|
||||||
.or_insert(Item::Table(Table::new()))
|
.or_insert(Item::Table(Table::new()))
|
||||||
.as_table_mut()
|
.as_table_mut()
|
||||||
|
@ -192,11 +223,7 @@ impl PyProjectTomlMut {
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
// Get or create `project.dependencies`.
|
// Get or create `project.dependencies`.
|
||||||
let dependencies = self
|
let dependencies = self
|
||||||
.doc
|
.doc()?
|
||||||
.entry("project")
|
|
||||||
.or_insert(Item::Table(Table::new()))
|
|
||||||
.as_table_mut()
|
|
||||||
.ok_or(Error::MalformedDependencies)?
|
|
||||||
.entry("dependencies")
|
.entry("dependencies")
|
||||||
.or_insert(Item::Value(Value::Array(Array::new())))
|
.or_insert(Item::Value(Value::Array(Array::new())))
|
||||||
.as_array_mut()
|
.as_array_mut()
|
||||||
|
@ -265,11 +292,7 @@ impl PyProjectTomlMut {
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
// Get or create `project.optional-dependencies`.
|
// Get or create `project.optional-dependencies`.
|
||||||
let optional_dependencies = self
|
let optional_dependencies = self
|
||||||
.doc
|
.doc()?
|
||||||
.entry("project")
|
|
||||||
.or_insert(Item::Table(Table::new()))
|
|
||||||
.as_table_mut()
|
|
||||||
.ok_or(Error::MalformedDependencies)?
|
|
||||||
.entry("optional-dependencies")
|
.entry("optional-dependencies")
|
||||||
.or_insert(Item::Table(Table::new()))
|
.or_insert(Item::Table(Table::new()))
|
||||||
.as_table_mut()
|
.as_table_mut()
|
||||||
|
@ -323,10 +346,7 @@ impl PyProjectTomlMut {
|
||||||
pub fn remove_dependency(&mut self, req: &PackageName) -> Result<Vec<Requirement>, Error> {
|
pub fn remove_dependency(&mut self, req: &PackageName) -> Result<Vec<Requirement>, Error> {
|
||||||
// Try to get `project.dependencies`.
|
// Try to get `project.dependencies`.
|
||||||
let Some(dependencies) = self
|
let Some(dependencies) = self
|
||||||
.doc
|
.doc_mut()?
|
||||||
.get_mut("project")
|
|
||||||
.map(|project| project.as_table_mut().ok_or(Error::MalformedSources))
|
|
||||||
.transpose()?
|
|
||||||
.and_then(|project| project.get_mut("dependencies"))
|
.and_then(|project| project.get_mut("dependencies"))
|
||||||
.map(|dependencies| dependencies.as_array_mut().ok_or(Error::MalformedSources))
|
.map(|dependencies| dependencies.as_array_mut().ok_or(Error::MalformedSources))
|
||||||
.transpose()?
|
.transpose()?
|
||||||
|
@ -372,10 +392,7 @@ impl PyProjectTomlMut {
|
||||||
) -> Result<Vec<Requirement>, Error> {
|
) -> Result<Vec<Requirement>, Error> {
|
||||||
// Try to get `project.optional-dependencies.<group>`.
|
// Try to get `project.optional-dependencies.<group>`.
|
||||||
let Some(optional_dependencies) = self
|
let Some(optional_dependencies) = self
|
||||||
.doc
|
.doc_mut()?
|
||||||
.get_mut("project")
|
|
||||||
.map(|project| project.as_table_mut().ok_or(Error::MalformedSources))
|
|
||||||
.transpose()?
|
|
||||||
.and_then(|project| project.get_mut("optional-dependencies"))
|
.and_then(|project| project.get_mut("optional-dependencies"))
|
||||||
.map(|extras| extras.as_table_mut().ok_or(Error::MalformedSources))
|
.map(|extras| extras.as_table_mut().ok_or(Error::MalformedSources))
|
||||||
.transpose()?
|
.transpose()?
|
||||||
|
|
|
@ -104,7 +104,7 @@ impl Workspace {
|
||||||
|
|
||||||
let pyproject_path = project_path.join("pyproject.toml");
|
let pyproject_path = project_path.join("pyproject.toml");
|
||||||
let contents = fs_err::tokio::read_to_string(&pyproject_path).await?;
|
let contents = fs_err::tokio::read_to_string(&pyproject_path).await?;
|
||||||
let pyproject_toml = PyProjectToml::from_string(contents)
|
let pyproject_toml = PyProjectToml::from_string(contents.clone())
|
||||||
.map_err(|err| WorkspaceError::Toml(pyproject_path.clone(), Box::new(err)))?;
|
.map_err(|err| WorkspaceError::Toml(pyproject_path.clone(), Box::new(err)))?;
|
||||||
|
|
||||||
// Check if the project is explicitly marked as unmanaged.
|
// Check if the project is explicitly marked as unmanaged.
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
use std::collections::hash_map::Entry;
|
use std::collections::hash_map::Entry;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use owo_colors::OwoColorize;
|
use owo_colors::OwoColorize;
|
||||||
use pep508_rs::{ExtraName, Requirement, VersionOrUrl};
|
|
||||||
use rustc_hash::{FxBuildHasher, FxHashMap};
|
use rustc_hash::{FxBuildHasher, FxHashMap};
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
|
||||||
|
use pep508_rs::{ExtraName, Requirement, VersionOrUrl};
|
||||||
use uv_auth::store_credentials_from_url;
|
use uv_auth::store_credentials_from_url;
|
||||||
use uv_cache::Cache;
|
use uv_cache::Cache;
|
||||||
use uv_client::{BaseClientBuilder, Connectivity, FlatIndexClient, RegistryClientBuilder};
|
use uv_client::{BaseClientBuilder, Connectivity, FlatIndexClient, RegistryClientBuilder};
|
||||||
|
@ -15,20 +17,24 @@ use uv_dispatch::BuildDispatch;
|
||||||
use uv_distribution::DistributionDatabase;
|
use uv_distribution::DistributionDatabase;
|
||||||
use uv_fs::CWD;
|
use uv_fs::CWD;
|
||||||
use uv_normalize::PackageName;
|
use uv_normalize::PackageName;
|
||||||
use uv_python::{PythonDownloads, PythonPreference, PythonRequest};
|
use uv_python::{
|
||||||
|
request_from_version_file, EnvironmentPreference, Interpreter, PythonDownloads,
|
||||||
|
PythonEnvironment, PythonInstallation, PythonPreference, PythonRequest, VersionRequest,
|
||||||
|
};
|
||||||
use uv_requirements::{NamedRequirementsResolver, RequirementsSource, RequirementsSpecification};
|
use uv_requirements::{NamedRequirementsResolver, RequirementsSource, RequirementsSpecification};
|
||||||
use uv_resolver::FlatIndex;
|
use uv_resolver::{FlatIndex, RequiresPython};
|
||||||
|
use uv_scripts::Pep723Script;
|
||||||
use uv_types::{BuildIsolation, HashStrategy};
|
use uv_types::{BuildIsolation, HashStrategy};
|
||||||
use uv_warnings::warn_user_once;
|
use uv_warnings::warn_user_once;
|
||||||
use uv_workspace::pyproject::{DependencyType, Source, SourceError};
|
use uv_workspace::pyproject::{DependencyType, Source, SourceError};
|
||||||
use uv_workspace::pyproject_mut::{ArrayEdit, PyProjectTomlMut};
|
use uv_workspace::pyproject_mut::{ArrayEdit, DependencyTarget, PyProjectTomlMut};
|
||||||
use uv_workspace::{DiscoveryOptions, VirtualProject, Workspace};
|
use uv_workspace::{DiscoveryOptions, VirtualProject, Workspace};
|
||||||
|
|
||||||
use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger};
|
use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger};
|
||||||
use crate::commands::pip::operations::Modifications;
|
use crate::commands::pip::operations::Modifications;
|
||||||
use crate::commands::pip::resolution_environment;
|
use crate::commands::pip::resolution_environment;
|
||||||
use crate::commands::project::ProjectError;
|
use crate::commands::project::ProjectError;
|
||||||
use crate::commands::reporters::ResolverReporter;
|
use crate::commands::reporters::{PythonDownloadReporter, ResolverReporter};
|
||||||
use crate::commands::{pip, project, ExitStatus, SharedState};
|
use crate::commands::{pip, project, ExitStatus, SharedState};
|
||||||
use crate::printer::Printer;
|
use crate::printer::Printer;
|
||||||
use crate::settings::ResolverInstallerSettings;
|
use crate::settings::ResolverInstallerSettings;
|
||||||
|
@ -50,6 +56,7 @@ pub(crate) async fn add(
|
||||||
package: Option<PackageName>,
|
package: Option<PackageName>,
|
||||||
python: Option<String>,
|
python: Option<String>,
|
||||||
settings: ResolverInstallerSettings,
|
settings: ResolverInstallerSettings,
|
||||||
|
script: Option<PathBuf>,
|
||||||
python_preference: PythonPreference,
|
python_preference: PythonPreference,
|
||||||
python_downloads: PythonDownloads,
|
python_downloads: PythonDownloads,
|
||||||
preview: PreviewMode,
|
preview: PreviewMode,
|
||||||
|
@ -63,6 +70,99 @@ pub(crate) async fn add(
|
||||||
warn_user_once!("`uv add` is experimental and may change without warning");
|
warn_user_once!("`uv add` is experimental and may change without warning");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let reporter = PythonDownloadReporter::single(printer);
|
||||||
|
|
||||||
|
let target = if let Some(script) = script {
|
||||||
|
// If we found a PEP 723 script and the user provided a project-only setting, warn.
|
||||||
|
if package.is_some() {
|
||||||
|
warn_user_once!(
|
||||||
|
"`--package` is a no-op for Python scripts with inline metadata, which always run in isolation"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if locked {
|
||||||
|
warn_user_once!(
|
||||||
|
"`--locked` is a no-op for Python scripts with inline metadata, which always run in isolation"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if frozen {
|
||||||
|
warn_user_once!(
|
||||||
|
"`--frozen` is a no-op for Python scripts with inline metadata, which always run in isolation"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if no_sync {
|
||||||
|
warn_user_once!(
|
||||||
|
"`--no_sync` is a no-op for Python scripts with inline metadata, which always run in isolation"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let client_builder = BaseClientBuilder::new()
|
||||||
|
.connectivity(connectivity)
|
||||||
|
.native_tls(native_tls);
|
||||||
|
|
||||||
|
// If we found a script, add to the existing metadata. Otherwise, create a new inline
|
||||||
|
// metadata tag.
|
||||||
|
let script = if let Some(script) = Pep723Script::read(&script).await? {
|
||||||
|
script
|
||||||
|
} else {
|
||||||
|
let python_request = if let Some(request) = python.as_deref() {
|
||||||
|
// (1) Explicit request from user
|
||||||
|
PythonRequest::parse(request)
|
||||||
|
} else if let Some(request) = request_from_version_file(&CWD).await? {
|
||||||
|
// (2) Request from `.python-version`
|
||||||
|
request
|
||||||
|
} else {
|
||||||
|
// (3) Assume any Python version
|
||||||
|
PythonRequest::Any
|
||||||
|
};
|
||||||
|
|
||||||
|
let interpreter = PythonInstallation::find_or_download(
|
||||||
|
Some(python_request),
|
||||||
|
EnvironmentPreference::Any,
|
||||||
|
python_preference,
|
||||||
|
python_downloads,
|
||||||
|
&client_builder,
|
||||||
|
cache,
|
||||||
|
Some(&reporter),
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.into_interpreter();
|
||||||
|
|
||||||
|
let requires_python =
|
||||||
|
RequiresPython::greater_than_equal_version(&interpreter.python_minor_version());
|
||||||
|
Pep723Script::create(&script, requires_python.specifiers()).await?
|
||||||
|
};
|
||||||
|
|
||||||
|
let python_request = if let Some(request) = python.as_deref() {
|
||||||
|
// (1) Explicit request from user
|
||||||
|
Some(PythonRequest::parse(request))
|
||||||
|
} else if let Some(request) = request_from_version_file(&CWD).await? {
|
||||||
|
// (2) Request from `.python-version`
|
||||||
|
Some(request)
|
||||||
|
} else {
|
||||||
|
// (3) `Requires-Python` in `pyproject.toml`
|
||||||
|
script
|
||||||
|
.metadata
|
||||||
|
.requires_python
|
||||||
|
.clone()
|
||||||
|
.map(|requires_python| {
|
||||||
|
PythonRequest::Version(VersionRequest::Range(requires_python))
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let interpreter = PythonInstallation::find_or_download(
|
||||||
|
python_request,
|
||||||
|
EnvironmentPreference::Any,
|
||||||
|
python_preference,
|
||||||
|
python_downloads,
|
||||||
|
&client_builder,
|
||||||
|
cache,
|
||||||
|
Some(&reporter),
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.into_interpreter();
|
||||||
|
|
||||||
|
Target::Script(script, Box::new(interpreter))
|
||||||
|
} else {
|
||||||
// Find the project in the workspace.
|
// Find the project in the workspace.
|
||||||
let project = if let Some(package) = package {
|
let project = if let Some(package) = package {
|
||||||
VirtualProject::Project(
|
VirtualProject::Project(
|
||||||
|
@ -101,6 +201,9 @@ pub(crate) async fn add(
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
Target::Project(project, venv)
|
||||||
|
};
|
||||||
|
|
||||||
let client_builder = BaseClientBuilder::new()
|
let client_builder = BaseClientBuilder::new()
|
||||||
.connectivity(connectivity)
|
.connectivity(connectivity)
|
||||||
.native_tls(native_tls)
|
.native_tls(native_tls)
|
||||||
|
@ -120,7 +223,7 @@ pub(crate) async fn add(
|
||||||
|
|
||||||
// Determine the environment for the resolution.
|
// Determine the environment for the resolution.
|
||||||
let (tags, markers) =
|
let (tags, markers) =
|
||||||
resolution_environment(python_version, python_platform, venv.interpreter())?;
|
resolution_environment(python_version, python_platform, target.interpreter())?;
|
||||||
|
|
||||||
// Add all authenticated sources to the cache.
|
// Add all authenticated sources to the cache.
|
||||||
for url in settings.index_locations.urls() {
|
for url in settings.index_locations.urls() {
|
||||||
|
@ -132,7 +235,7 @@ pub(crate) async fn add(
|
||||||
.index_urls(settings.index_locations.index_urls())
|
.index_urls(settings.index_locations.index_urls())
|
||||||
.index_strategy(settings.index_strategy)
|
.index_strategy(settings.index_strategy)
|
||||||
.markers(&markers)
|
.markers(&markers)
|
||||||
.platform(venv.interpreter().platform())
|
.platform(target.interpreter().platform())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
// Initialize any shared state.
|
// Initialize any shared state.
|
||||||
|
@ -153,7 +256,7 @@ pub(crate) async fn add(
|
||||||
&client,
|
&client,
|
||||||
cache,
|
cache,
|
||||||
&build_constraints,
|
&build_constraints,
|
||||||
venv.interpreter(),
|
target.interpreter(),
|
||||||
&settings.index_locations,
|
&settings.index_locations,
|
||||||
&flat_index,
|
&flat_index,
|
||||||
&state.index,
|
&state.index,
|
||||||
|
@ -182,9 +285,16 @@ pub(crate) async fn add(
|
||||||
.resolve()
|
.resolve()
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// Add the requirements to the `pyproject.toml`.
|
// Add the requirements to the `pyproject.toml` or script.
|
||||||
let existing = project.pyproject_toml();
|
let mut toml = match &target {
|
||||||
let mut pyproject = PyProjectTomlMut::from_toml(existing)?;
|
Target::Script(script, _) => {
|
||||||
|
PyProjectTomlMut::from_toml(&script.metadata.raw, DependencyTarget::Script)
|
||||||
|
}
|
||||||
|
Target::Project(project, _) => PyProjectTomlMut::from_toml(
|
||||||
|
&project.pyproject_toml().raw,
|
||||||
|
DependencyTarget::PyProjectToml,
|
||||||
|
),
|
||||||
|
}?;
|
||||||
let mut edits = Vec::with_capacity(requirements.len());
|
let mut edits = Vec::with_capacity(requirements.len());
|
||||||
for mut requirement in requirements {
|
for mut requirement in requirements {
|
||||||
// Add the specified extras.
|
// Add the specified extras.
|
||||||
|
@ -192,48 +302,40 @@ pub(crate) async fn add(
|
||||||
requirement.extras.sort_unstable();
|
requirement.extras.sort_unstable();
|
||||||
requirement.extras.dedup();
|
requirement.extras.dedup();
|
||||||
|
|
||||||
let (requirement, source) = if raw_sources {
|
let (requirement, source) = match target {
|
||||||
// Use the PEP 508 requirement directly.
|
Target::Script(_, _) | Target::Project(_, _) if raw_sources => {
|
||||||
(pep508_rs::Requirement::from(requirement), None)
|
(pep508_rs::Requirement::from(requirement), None)
|
||||||
} else {
|
}
|
||||||
// Otherwise, try to construct the source.
|
Target::Script(_, _) => resolve_requirement(
|
||||||
|
requirement,
|
||||||
|
false,
|
||||||
|
editable,
|
||||||
|
rev.clone(),
|
||||||
|
tag.clone(),
|
||||||
|
branch.clone(),
|
||||||
|
)?,
|
||||||
|
Target::Project(ref project, _) => {
|
||||||
let workspace = project
|
let workspace = project
|
||||||
.workspace()
|
.workspace()
|
||||||
.packages()
|
.packages()
|
||||||
.contains_key(&requirement.name);
|
.contains_key(&requirement.name);
|
||||||
let result = Source::from_requirement(
|
resolve_requirement(
|
||||||
&requirement.name,
|
requirement,
|
||||||
requirement.source.clone(),
|
|
||||||
workspace,
|
workspace,
|
||||||
editable,
|
editable,
|
||||||
rev.clone(),
|
rev.clone(),
|
||||||
tag.clone(),
|
tag.clone(),
|
||||||
branch.clone(),
|
branch.clone(),
|
||||||
);
|
)?
|
||||||
|
|
||||||
let source = match result {
|
|
||||||
Ok(source) => source,
|
|
||||||
Err(SourceError::UnresolvedReference(rev)) => {
|
|
||||||
anyhow::bail!("Cannot resolve Git reference `{rev}` for requirement `{name}`. Specify the reference with one of `--tag`, `--branch`, or `--rev`, or use the `--raw-sources` flag.", name = requirement.name)
|
|
||||||
}
|
}
|
||||||
Err(err) => return Err(err.into()),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Ignore the PEP 508 source.
|
|
||||||
let mut requirement = pep508_rs::Requirement::from(requirement);
|
|
||||||
requirement.clear_url();
|
|
||||||
|
|
||||||
(requirement, source)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update the `pyproject.toml`.
|
// Update the `pyproject.toml`.
|
||||||
let edit = match dependency_type {
|
let edit = match dependency_type {
|
||||||
DependencyType::Production => {
|
DependencyType::Production => toml.add_dependency(&requirement, source.as_ref())?,
|
||||||
pyproject.add_dependency(&requirement, source.as_ref())?
|
DependencyType::Dev => toml.add_dev_dependency(&requirement, source.as_ref())?,
|
||||||
}
|
|
||||||
DependencyType::Dev => pyproject.add_dev_dependency(&requirement, source.as_ref())?,
|
|
||||||
DependencyType::Optional(ref group) => {
|
DependencyType::Optional(ref group) => {
|
||||||
pyproject.add_optional_dependency(group, &requirement, source.as_ref())?
|
toml.add_optional_dependency(group, &requirement, source.as_ref())?
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -246,15 +348,35 @@ pub(crate) async fn add(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save the modified `pyproject.toml`.
|
let content = toml.to_string();
|
||||||
let mut modified = false;
|
|
||||||
let content = pyproject.to_string();
|
// Save the modified `pyproject.toml` or script.
|
||||||
if content == existing.raw {
|
let modified = match &target {
|
||||||
debug!("No changes to `pyproject.toml`; skipping update");
|
Target::Script(script, _) => {
|
||||||
|
if content == script.metadata.raw {
|
||||||
|
debug!("No changes to dependencies; skipping update");
|
||||||
|
false
|
||||||
} else {
|
} else {
|
||||||
fs_err::write(project.root().join("pyproject.toml"), &content)?;
|
script.write(&content).await?;
|
||||||
modified = true;
|
true
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
Target::Project(project, _) => {
|
||||||
|
if content == *project.pyproject_toml().raw {
|
||||||
|
debug!("No changes to dependencies; skipping update");
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
let pyproject_path = project.root().join("pyproject.toml");
|
||||||
|
fs_err::write(pyproject_path, &content)?;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// If `--script`, exit early. There's no reason to lock and sync.
|
||||||
|
let Target::Project(project, venv) = target else {
|
||||||
|
return Ok(ExitStatus::Success);
|
||||||
|
};
|
||||||
|
|
||||||
// If `--frozen`, exit early. There's no reason to lock and sync, and we don't need a `uv.lock`
|
// If `--frozen`, exit early. There's no reason to lock and sync, and we don't need a `uv.lock`
|
||||||
// to exist at all.
|
// to exist at all.
|
||||||
|
@ -262,6 +384,8 @@ pub(crate) async fn add(
|
||||||
return Ok(ExitStatus::Success);
|
return Ok(ExitStatus::Success);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let existing = project.pyproject_toml();
|
||||||
|
|
||||||
// Update the `pypackage.toml` in-memory.
|
// Update the `pypackage.toml` in-memory.
|
||||||
let project = project
|
let project = project
|
||||||
.clone()
|
.clone()
|
||||||
|
@ -357,13 +481,13 @@ pub(crate) async fn add(
|
||||||
|
|
||||||
match edit.dependency_type {
|
match edit.dependency_type {
|
||||||
DependencyType::Production => {
|
DependencyType::Production => {
|
||||||
pyproject.set_dependency_minimum_version(*index, minimum)?;
|
toml.set_dependency_minimum_version(*index, minimum)?;
|
||||||
}
|
}
|
||||||
DependencyType::Dev => {
|
DependencyType::Dev => {
|
||||||
pyproject.set_dev_dependency_minimum_version(*index, minimum)?;
|
toml.set_dev_dependency_minimum_version(*index, minimum)?;
|
||||||
}
|
}
|
||||||
DependencyType::Optional(ref group) => {
|
DependencyType::Optional(ref group) => {
|
||||||
pyproject.set_optional_dependency_minimum_version(group, *index, minimum)?;
|
toml.set_optional_dependency_minimum_version(group, *index, minimum)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -374,7 +498,7 @@ pub(crate) async fn add(
|
||||||
// string content, since the above loop _must_ change an empty specifier to a non-empty
|
// string content, since the above loop _must_ change an empty specifier to a non-empty
|
||||||
// specifier.
|
// specifier.
|
||||||
if modified {
|
if modified {
|
||||||
fs_err::write(project.root().join("pyproject.toml"), pyproject.to_string())?;
|
fs_err::write(project.root().join("pyproject.toml"), toml.to_string())?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -426,6 +550,62 @@ pub(crate) async fn add(
|
||||||
Ok(ExitStatus::Success)
|
Ok(ExitStatus::Success)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolves the source for a requirement and processes it into a PEP 508 compliant format.
|
||||||
|
fn resolve_requirement(
|
||||||
|
requirement: pypi_types::Requirement,
|
||||||
|
workspace: bool,
|
||||||
|
editable: Option<bool>,
|
||||||
|
rev: Option<String>,
|
||||||
|
tag: Option<String>,
|
||||||
|
branch: Option<String>,
|
||||||
|
) -> Result<(Requirement, Option<Source>), anyhow::Error> {
|
||||||
|
let result = Source::from_requirement(
|
||||||
|
&requirement.name,
|
||||||
|
requirement.source.clone(),
|
||||||
|
workspace,
|
||||||
|
editable,
|
||||||
|
rev,
|
||||||
|
tag,
|
||||||
|
branch,
|
||||||
|
);
|
||||||
|
|
||||||
|
let source = match result {
|
||||||
|
Ok(source) => source,
|
||||||
|
Err(SourceError::UnresolvedReference(rev)) => {
|
||||||
|
anyhow::bail!(
|
||||||
|
"Cannot resolve Git reference `{rev}` for requirement `{name}`. Specify the reference with one of `--tag`, `--branch`, or `--rev`, or use the `--raw-sources` flag.",
|
||||||
|
name = requirement.name
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Err(err) => return Err(err.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ignore the PEP 508 source by clearing the URL.
|
||||||
|
let mut processed_requirement = pep508_rs::Requirement::from(requirement);
|
||||||
|
processed_requirement.clear_url();
|
||||||
|
|
||||||
|
Ok((processed_requirement, source))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents the destination where dependencies are added, either to a project or a script.
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum Target {
|
||||||
|
/// A PEP 723 script, with inline metadata.
|
||||||
|
Script(Pep723Script, Box<Interpreter>),
|
||||||
|
/// A project with a `pyproject.toml`.
|
||||||
|
Project(VirtualProject, PythonEnvironment),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Target {
|
||||||
|
/// Returns the [`Interpreter`] for the target.
|
||||||
|
fn interpreter(&self) -> &Interpreter {
|
||||||
|
match self {
|
||||||
|
Self::Script(_, interpreter) => interpreter,
|
||||||
|
Self::Project(_, venv) => venv.interpreter(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct DependencyEdit<'a> {
|
struct DependencyEdit<'a> {
|
||||||
dependency_type: &'a DependencyType,
|
dependency_type: &'a DependencyType,
|
||||||
|
|
|
@ -16,7 +16,7 @@ use uv_python::{
|
||||||
};
|
};
|
||||||
use uv_resolver::RequiresPython;
|
use uv_resolver::RequiresPython;
|
||||||
use uv_warnings::warn_user_once;
|
use uv_warnings::warn_user_once;
|
||||||
use uv_workspace::pyproject_mut::PyProjectTomlMut;
|
use uv_workspace::pyproject_mut::{DependencyTarget, PyProjectTomlMut};
|
||||||
use uv_workspace::{check_nested_workspaces, DiscoveryOptions, Workspace, WorkspaceError};
|
use uv_workspace::{check_nested_workspaces, DiscoveryOptions, Workspace, WorkspaceError};
|
||||||
|
|
||||||
use crate::commands::project::find_requires_python;
|
use crate::commands::project::find_requires_python;
|
||||||
|
@ -315,7 +315,10 @@ async fn init_project(
|
||||||
)?;
|
)?;
|
||||||
} else {
|
} else {
|
||||||
// Add the package to the workspace.
|
// Add the package to the workspace.
|
||||||
let mut pyproject = PyProjectTomlMut::from_toml(workspace.pyproject_toml())?;
|
let mut pyproject = PyProjectTomlMut::from_toml(
|
||||||
|
&workspace.pyproject_toml().raw,
|
||||||
|
DependencyTarget::PyProjectToml,
|
||||||
|
)?;
|
||||||
pyproject.add_workspace(path.strip_prefix(workspace.install_path())?)?;
|
pyproject.add_workspace(path.strip_prefix(workspace.install_path())?)?;
|
||||||
|
|
||||||
// Save the modified `pyproject.toml`.
|
// Save the modified `pyproject.toml`.
|
||||||
|
|
|
@ -6,10 +6,11 @@ use uv_client::Connectivity;
|
||||||
use uv_configuration::{Concurrency, ExtrasSpecification, PreviewMode};
|
use uv_configuration::{Concurrency, ExtrasSpecification, PreviewMode};
|
||||||
use uv_fs::CWD;
|
use uv_fs::CWD;
|
||||||
use uv_python::{PythonDownloads, PythonPreference, PythonRequest};
|
use uv_python::{PythonDownloads, PythonPreference, PythonRequest};
|
||||||
|
use uv_scripts::Pep723Script;
|
||||||
use uv_warnings::{warn_user, warn_user_once};
|
use uv_warnings::{warn_user, warn_user_once};
|
||||||
use uv_workspace::pyproject::DependencyType;
|
use uv_workspace::pyproject::DependencyType;
|
||||||
use uv_workspace::pyproject_mut::PyProjectTomlMut;
|
use uv_workspace::pyproject_mut::{DependencyTarget, PyProjectTomlMut};
|
||||||
use uv_workspace::{DiscoveryOptions, ProjectWorkspace, VirtualProject, Workspace};
|
use uv_workspace::{DiscoveryOptions, VirtualProject, Workspace};
|
||||||
|
|
||||||
use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger};
|
use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger};
|
||||||
use crate::commands::pip::operations::Modifications;
|
use crate::commands::pip::operations::Modifications;
|
||||||
|
@ -28,6 +29,7 @@ pub(crate) async fn remove(
|
||||||
package: Option<PackageName>,
|
package: Option<PackageName>,
|
||||||
python: Option<String>,
|
python: Option<String>,
|
||||||
settings: ResolverInstallerSettings,
|
settings: ResolverInstallerSettings,
|
||||||
|
script: Option<Pep723Script>,
|
||||||
python_preference: PythonPreference,
|
python_preference: PythonPreference,
|
||||||
python_downloads: PythonDownloads,
|
python_downloads: PythonDownloads,
|
||||||
preview: PreviewMode,
|
preview: PreviewMode,
|
||||||
|
@ -41,41 +43,79 @@ pub(crate) async fn remove(
|
||||||
warn_user_once!("`uv remove` is experimental and may change without warning");
|
warn_user_once!("`uv remove` is experimental and may change without warning");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let target = if let Some(script) = script {
|
||||||
|
// If we found a PEP 723 script and the user provided a project-only setting, warn.
|
||||||
|
if package.is_some() {
|
||||||
|
warn_user_once!(
|
||||||
|
"`--package` is a no-op for Python scripts with inline metadata, which always run in isolation"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if locked {
|
||||||
|
warn_user_once!(
|
||||||
|
"`--locked` is a no-op for Python scripts with inline metadata, which always run in isolation"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if frozen {
|
||||||
|
warn_user_once!(
|
||||||
|
"`--frozen` is a no-op for Python scripts with inline metadata, which always run in isolation"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if no_sync {
|
||||||
|
warn_user_once!(
|
||||||
|
"`--no_sync` is a no-op for Python scripts with inline metadata, which always run in isolation"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Target::Script(script)
|
||||||
|
} else {
|
||||||
// Find the project in the workspace.
|
// Find the project in the workspace.
|
||||||
let project = if let Some(package) = package {
|
let project = if let Some(package) = package {
|
||||||
|
VirtualProject::Project(
|
||||||
Workspace::discover(&CWD, &DiscoveryOptions::default())
|
Workspace::discover(&CWD, &DiscoveryOptions::default())
|
||||||
.await?
|
.await?
|
||||||
.with_current_project(package.clone())
|
.with_current_project(package.clone())
|
||||||
.with_context(|| format!("Package `{package}` not found in workspace"))?
|
.with_context(|| format!("Package `{package}` not found in workspace"))?,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
ProjectWorkspace::discover(&CWD, &DiscoveryOptions::default()).await?
|
VirtualProject::discover(&CWD, &DiscoveryOptions::default()).await?
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut pyproject = PyProjectTomlMut::from_toml(project.current_project().pyproject_toml())?;
|
Target::Project(project)
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut toml = match &target {
|
||||||
|
Target::Script(script) => {
|
||||||
|
PyProjectTomlMut::from_toml(&script.metadata.raw, DependencyTarget::Script)
|
||||||
|
}
|
||||||
|
Target::Project(project) => PyProjectTomlMut::from_toml(
|
||||||
|
project.pyproject_toml().raw.as_ref(),
|
||||||
|
DependencyTarget::PyProjectToml,
|
||||||
|
),
|
||||||
|
}?;
|
||||||
|
|
||||||
for package in packages {
|
for package in packages {
|
||||||
match dependency_type {
|
match dependency_type {
|
||||||
DependencyType::Production => {
|
DependencyType::Production => {
|
||||||
let deps = pyproject.remove_dependency(&package)?;
|
let deps = toml.remove_dependency(&package)?;
|
||||||
if deps.is_empty() {
|
if deps.is_empty() {
|
||||||
warn_if_present(&package, &pyproject);
|
warn_if_present(&package, &toml);
|
||||||
anyhow::bail!(
|
anyhow::bail!(
|
||||||
"The dependency `{package}` could not be found in `dependencies`"
|
"The dependency `{package}` could not be found in `dependencies`"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
DependencyType::Dev => {
|
DependencyType::Dev => {
|
||||||
let deps = pyproject.remove_dev_dependency(&package)?;
|
let deps = toml.remove_dev_dependency(&package)?;
|
||||||
if deps.is_empty() {
|
if deps.is_empty() {
|
||||||
warn_if_present(&package, &pyproject);
|
warn_if_present(&package, &toml);
|
||||||
anyhow::bail!(
|
anyhow::bail!(
|
||||||
"The dependency `{package}` could not be found in `dev-dependencies`"
|
"The dependency `{package}` could not be found in `dev-dependencies`"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
DependencyType::Optional(ref group) => {
|
DependencyType::Optional(ref group) => {
|
||||||
let deps = pyproject.remove_optional_dependency(&package, group)?;
|
let deps = toml.remove_optional_dependency(&package, group)?;
|
||||||
if deps.is_empty() {
|
if deps.is_empty() {
|
||||||
warn_if_present(&package, &pyproject);
|
warn_if_present(&package, &toml);
|
||||||
anyhow::bail!(
|
anyhow::bail!(
|
||||||
"The dependency `{package}` could not be found in `optional-dependencies`"
|
"The dependency `{package}` could not be found in `optional-dependencies`"
|
||||||
);
|
);
|
||||||
|
@ -84,11 +124,16 @@ pub(crate) async fn remove(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save the modified `pyproject.toml`.
|
// Save the modified dependencies.
|
||||||
fs_err::write(
|
match &target {
|
||||||
project.current_project().root().join("pyproject.toml"),
|
Target::Script(script) => {
|
||||||
pyproject.to_string(),
|
script.write(&toml.to_string()).await?;
|
||||||
)?;
|
}
|
||||||
|
Target::Project(project) => {
|
||||||
|
let pyproject_path = project.root().join("pyproject.toml");
|
||||||
|
fs_err::write(pyproject_path, toml.to_string())?;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// If `--frozen`, exit early. There's no reason to lock and sync, and we don't need a `uv.lock`
|
// If `--frozen`, exit early. There's no reason to lock and sync, and we don't need a `uv.lock`
|
||||||
// to exist at all.
|
// to exist at all.
|
||||||
|
@ -96,6 +141,11 @@ pub(crate) async fn remove(
|
||||||
return Ok(ExitStatus::Success);
|
return Ok(ExitStatus::Success);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If `--script`, exit early. There's no reason to lock and sync.
|
||||||
|
let Target::Project(project) = target else {
|
||||||
|
return Ok(ExitStatus::Success);
|
||||||
|
};
|
||||||
|
|
||||||
// Discover or create the virtual environment.
|
// Discover or create the virtual environment.
|
||||||
let venv = project::get_or_init_environment(
|
let venv = project::get_or_init_environment(
|
||||||
project.workspace(),
|
project.workspace(),
|
||||||
|
@ -139,7 +189,7 @@ pub(crate) async fn remove(
|
||||||
let state = SharedState::default();
|
let state = SharedState::default();
|
||||||
|
|
||||||
project::sync::do_sync(
|
project::sync::do_sync(
|
||||||
&VirtualProject::Project(project),
|
&project,
|
||||||
&venv,
|
&venv,
|
||||||
&lock.lock,
|
&lock.lock,
|
||||||
&extras,
|
&extras,
|
||||||
|
@ -160,6 +210,15 @@ pub(crate) async fn remove(
|
||||||
Ok(ExitStatus::Success)
|
Ok(ExitStatus::Success)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Represents the destination where dependencies are added, either to a project or a script.
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum Target {
|
||||||
|
/// A PEP 723 script, with inline metadata.
|
||||||
|
Project(VirtualProject),
|
||||||
|
/// A project with a `pyproject.toml`.
|
||||||
|
Script(Pep723Script),
|
||||||
|
}
|
||||||
|
|
||||||
/// Emit a warning if a dependency with the given name is present as any dependency type.
|
/// Emit a warning if a dependency with the given name is present as any dependency type.
|
||||||
///
|
///
|
||||||
/// This is useful when a dependency of the user-specified type was not found, but it may be present
|
/// This is useful when a dependency of the user-specified type was not found, but it may be present
|
||||||
|
|
|
@ -135,6 +135,12 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
|
||||||
let script = if let Commands::Project(command) = &*cli.command {
|
let script = if let Commands::Project(command) = &*cli.command {
|
||||||
if let ProjectCommand::Run(uv_cli::RunArgs { command, .. }) = &**command {
|
if let ProjectCommand::Run(uv_cli::RunArgs { command, .. }) = &**command {
|
||||||
parse_script(command).await?
|
parse_script(command).await?
|
||||||
|
} else if let ProjectCommand::Remove(uv_cli::RemoveArgs {
|
||||||
|
script: Some(script),
|
||||||
|
..
|
||||||
|
}) = &**command
|
||||||
|
{
|
||||||
|
Pep723Script::read(&script).await?
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
@ -1157,6 +1163,7 @@ async fn run_project(
|
||||||
args.package,
|
args.package,
|
||||||
args.python,
|
args.python,
|
||||||
args.settings,
|
args.settings,
|
||||||
|
args.script,
|
||||||
globals.python_preference,
|
globals.python_preference,
|
||||||
globals.python_downloads,
|
globals.python_downloads,
|
||||||
globals.preview,
|
globals.preview,
|
||||||
|
@ -1189,6 +1196,7 @@ async fn run_project(
|
||||||
args.package,
|
args.package,
|
||||||
args.python,
|
args.python,
|
||||||
args.settings,
|
args.settings,
|
||||||
|
script,
|
||||||
globals.python_preference,
|
globals.python_preference,
|
||||||
globals.python_downloads,
|
globals.python_downloads,
|
||||||
globals.preview,
|
globals.preview,
|
||||||
|
|
|
@ -703,6 +703,7 @@ pub(crate) struct AddSettings {
|
||||||
pub(crate) tag: Option<String>,
|
pub(crate) tag: Option<String>,
|
||||||
pub(crate) branch: Option<String>,
|
pub(crate) branch: Option<String>,
|
||||||
pub(crate) package: Option<PackageName>,
|
pub(crate) package: Option<PackageName>,
|
||||||
|
pub(crate) script: Option<PathBuf>,
|
||||||
pub(crate) python: Option<String>,
|
pub(crate) python: Option<String>,
|
||||||
pub(crate) refresh: Refresh,
|
pub(crate) refresh: Refresh,
|
||||||
pub(crate) settings: ResolverInstallerSettings,
|
pub(crate) settings: ResolverInstallerSettings,
|
||||||
|
@ -730,6 +731,7 @@ impl AddSettings {
|
||||||
build,
|
build,
|
||||||
refresh,
|
refresh,
|
||||||
package,
|
package,
|
||||||
|
script,
|
||||||
python,
|
python,
|
||||||
} = args;
|
} = args;
|
||||||
|
|
||||||
|
@ -757,6 +759,7 @@ impl AddSettings {
|
||||||
tag,
|
tag,
|
||||||
branch,
|
branch,
|
||||||
package,
|
package,
|
||||||
|
script,
|
||||||
python,
|
python,
|
||||||
editable: flag(editable, no_editable),
|
editable: flag(editable, no_editable),
|
||||||
extras: extra.unwrap_or_default(),
|
extras: extra.unwrap_or_default(),
|
||||||
|
@ -779,6 +782,7 @@ pub(crate) struct RemoveSettings {
|
||||||
pub(crate) packages: Vec<PackageName>,
|
pub(crate) packages: Vec<PackageName>,
|
||||||
pub(crate) dependency_type: DependencyType,
|
pub(crate) dependency_type: DependencyType,
|
||||||
pub(crate) package: Option<PackageName>,
|
pub(crate) package: Option<PackageName>,
|
||||||
|
pub(crate) script: Option<PathBuf>,
|
||||||
pub(crate) python: Option<String>,
|
pub(crate) python: Option<String>,
|
||||||
pub(crate) refresh: Refresh,
|
pub(crate) refresh: Refresh,
|
||||||
pub(crate) settings: ResolverInstallerSettings,
|
pub(crate) settings: ResolverInstallerSettings,
|
||||||
|
@ -799,6 +803,7 @@ impl RemoveSettings {
|
||||||
build,
|
build,
|
||||||
refresh,
|
refresh,
|
||||||
package,
|
package,
|
||||||
|
script,
|
||||||
python,
|
python,
|
||||||
} = args;
|
} = args;
|
||||||
|
|
||||||
|
@ -817,6 +822,7 @@ impl RemoveSettings {
|
||||||
packages,
|
packages,
|
||||||
dependency_type,
|
dependency_type,
|
||||||
package,
|
package,
|
||||||
|
script,
|
||||||
python,
|
python,
|
||||||
refresh: Refresh::from(refresh),
|
refresh: Refresh::from(refresh),
|
||||||
settings: ResolverInstallerSettings::combine(
|
settings: ResolverInstallerSettings::combine(
|
||||||
|
|
|
@ -2912,3 +2912,412 @@ fn add_repeat() -> Result<()> {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Add to a PEP 732 script.
|
||||||
|
#[test]
|
||||||
|
fn add_script() -> Result<()> {
|
||||||
|
let context = TestContext::new("3.12");
|
||||||
|
|
||||||
|
let script = context.temp_dir.child("script.py");
|
||||||
|
script.write_str(indoc! {r#"
|
||||||
|
# /// 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])
|
||||||
|
"#})?;
|
||||||
|
|
||||||
|
uv_snapshot!(context.filters(), context.add(&["anyio"]).arg("--script").arg(script.path()), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
warning: `uv add` is experimental and may change without warning
|
||||||
|
"###);
|
||||||
|
|
||||||
|
let script_content = fs_err::read_to_string(context.temp_dir.join("script.py"))?;
|
||||||
|
|
||||||
|
insta::with_settings!({
|
||||||
|
filters => context.filters(),
|
||||||
|
}, {
|
||||||
|
assert_snapshot!(
|
||||||
|
script_content, @r###"
|
||||||
|
# /// script
|
||||||
|
# requires-python = ">=3.11"
|
||||||
|
# dependencies = [
|
||||||
|
# "requests<3",
|
||||||
|
# "rich",
|
||||||
|
# "anyio",
|
||||||
|
# ]
|
||||||
|
# ///
|
||||||
|
|
||||||
|
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])
|
||||||
|
"###
|
||||||
|
);
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add to a script without an existing metadata table.
|
||||||
|
#[test]
|
||||||
|
fn add_script_without_metadata_table() -> Result<()> {
|
||||||
|
let context = TestContext::new("3.12");
|
||||||
|
|
||||||
|
let script = context.temp_dir.child("script.py");
|
||||||
|
script.write_str(indoc! {r#"
|
||||||
|
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])
|
||||||
|
"#})?;
|
||||||
|
|
||||||
|
uv_snapshot!(context.filters(), context.add(&["rich", "requests<3"]).arg("--script").arg(script.path()), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
warning: `uv add` is experimental and may change without warning
|
||||||
|
"###);
|
||||||
|
|
||||||
|
let script_content = fs_err::read_to_string(context.temp_dir.join("script.py"))?;
|
||||||
|
|
||||||
|
insta::with_settings!({
|
||||||
|
filters => context.filters(),
|
||||||
|
}, {
|
||||||
|
assert_snapshot!(
|
||||||
|
script_content, @r###"
|
||||||
|
# /// script
|
||||||
|
# requires-python = ">=3.12"
|
||||||
|
# dependencies = [
|
||||||
|
# "rich",
|
||||||
|
# "requests<3",
|
||||||
|
# ]
|
||||||
|
# ///
|
||||||
|
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])
|
||||||
|
"###
|
||||||
|
);
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add to a script without an existing metadata table, but with a shebang.
|
||||||
|
#[test]
|
||||||
|
fn add_script_without_metadata_table_with_shebang() -> Result<()> {
|
||||||
|
let context = TestContext::new("3.12");
|
||||||
|
|
||||||
|
let script = context.temp_dir.child("script.py");
|
||||||
|
script.write_str(indoc! {r#"
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
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])
|
||||||
|
"#})?;
|
||||||
|
|
||||||
|
uv_snapshot!(context.filters(), context.add(&["rich", "requests<3"]).arg("--script").arg(script.path()), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
warning: `uv add` is experimental and may change without warning
|
||||||
|
"###);
|
||||||
|
|
||||||
|
let script_content = fs_err::read_to_string(context.temp_dir.join("script.py"))?;
|
||||||
|
|
||||||
|
insta::with_settings!({
|
||||||
|
filters => context.filters(),
|
||||||
|
}, {
|
||||||
|
assert_snapshot!(
|
||||||
|
script_content, @r###"
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# /// script
|
||||||
|
# requires-python = ">=3.12"
|
||||||
|
# dependencies = [
|
||||||
|
# "rich",
|
||||||
|
# "requests<3",
|
||||||
|
# ]
|
||||||
|
# ///
|
||||||
|
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])
|
||||||
|
"###
|
||||||
|
);
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add to a script without a metadata table, but with a docstring.
|
||||||
|
#[test]
|
||||||
|
fn add_script_without_metadata_table_with_docstring() -> Result<()> {
|
||||||
|
let context = TestContext::new("3.12");
|
||||||
|
|
||||||
|
let script = context.temp_dir.child("script.py");
|
||||||
|
script.write_str(indoc! {r#"
|
||||||
|
"""This is a script."""
|
||||||
|
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])
|
||||||
|
"#})?;
|
||||||
|
|
||||||
|
uv_snapshot!(context.filters(), context.add(&["rich", "requests<3"]).arg("--script").arg(script.path()), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
warning: `uv add` is experimental and may change without warning
|
||||||
|
"###);
|
||||||
|
|
||||||
|
let script_content = fs_err::read_to_string(context.temp_dir.join("script.py"))?;
|
||||||
|
|
||||||
|
insta::with_settings!({
|
||||||
|
filters => context.filters(),
|
||||||
|
}, {
|
||||||
|
assert_snapshot!(
|
||||||
|
script_content, @r###"
|
||||||
|
# /// script
|
||||||
|
# requires-python = ">=3.12"
|
||||||
|
# dependencies = [
|
||||||
|
# "rich",
|
||||||
|
# "requests<3",
|
||||||
|
# ]
|
||||||
|
# ///
|
||||||
|
"""This is a script."""
|
||||||
|
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])
|
||||||
|
"###
|
||||||
|
);
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove from a PEP732 script,
|
||||||
|
#[test]
|
||||||
|
fn remove_script() -> Result<()> {
|
||||||
|
let context = TestContext::new("3.12");
|
||||||
|
|
||||||
|
let script = context.temp_dir.child("script.py");
|
||||||
|
script.write_str(indoc! {r#"
|
||||||
|
# /// script
|
||||||
|
# requires-python = ">=3.11"
|
||||||
|
# dependencies = [
|
||||||
|
# "requests<3",
|
||||||
|
# "rich",
|
||||||
|
# "anyio",
|
||||||
|
# ]
|
||||||
|
# ///
|
||||||
|
|
||||||
|
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])
|
||||||
|
"#})?;
|
||||||
|
|
||||||
|
uv_snapshot!(context.filters(), context.remove(&["anyio"]).arg("--script").arg(script.path()), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
warning: `uv remove` is experimental and may change without warning
|
||||||
|
"###);
|
||||||
|
|
||||||
|
let script_content = fs_err::read_to_string(context.temp_dir.join("script.py"))?;
|
||||||
|
|
||||||
|
insta::with_settings!({
|
||||||
|
filters => context.filters(),
|
||||||
|
}, {
|
||||||
|
assert_snapshot!(
|
||||||
|
script_content, @r###"
|
||||||
|
# /// 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])
|
||||||
|
"###
|
||||||
|
);
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove last dependency PEP732 script
|
||||||
|
#[test]
|
||||||
|
fn remove_last_dep_script() -> Result<()> {
|
||||||
|
let context = TestContext::new("3.12");
|
||||||
|
|
||||||
|
let script = context.temp_dir.child("script.py");
|
||||||
|
script.write_str(indoc! {r#"
|
||||||
|
# /// script
|
||||||
|
# requires-python = ">=3.11"
|
||||||
|
# dependencies = [
|
||||||
|
# "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])
|
||||||
|
"#})?;
|
||||||
|
|
||||||
|
uv_snapshot!(context.filters(), context.remove(&["rich"]).arg("--script").arg(script.path()), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
warning: `uv remove` is experimental and may change without warning
|
||||||
|
"###);
|
||||||
|
|
||||||
|
let script_content = fs_err::read_to_string(context.temp_dir.join("script.py"))?;
|
||||||
|
|
||||||
|
insta::with_settings!({
|
||||||
|
filters => context.filters(),
|
||||||
|
}, {
|
||||||
|
assert_snapshot!(
|
||||||
|
script_content, @r###"
|
||||||
|
# /// script
|
||||||
|
# requires-python = ">=3.11"
|
||||||
|
# dependencies = []
|
||||||
|
# ///
|
||||||
|
|
||||||
|
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])
|
||||||
|
"###
|
||||||
|
);
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a Git requirement to PEP732 script.
|
||||||
|
#[test]
|
||||||
|
#[cfg(feature = "git")]
|
||||||
|
fn add_git_to_script() -> Result<()> {
|
||||||
|
let context = TestContext::new("3.12");
|
||||||
|
|
||||||
|
let script = context.temp_dir.child("script.py");
|
||||||
|
script.write_str(indoc! {r#"
|
||||||
|
# /// script
|
||||||
|
# requires-python = ">=3.11"
|
||||||
|
# dependencies = [
|
||||||
|
# "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])
|
||||||
|
"#})?;
|
||||||
|
|
||||||
|
// Adding with an ambiguous Git reference will fail.
|
||||||
|
uv_snapshot!(context.filters(), context
|
||||||
|
.add(&["uv-public-pypackage @ git+https://github.com/astral-test/uv-public-pypackage@0.0.1"])
|
||||||
|
.arg("--preview")
|
||||||
|
.arg("--script")
|
||||||
|
.arg("script.py"), @r###"
|
||||||
|
success: false
|
||||||
|
exit_code: 2
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
error: Cannot resolve Git reference `0.0.1` for requirement `uv-public-pypackage`. Specify the reference with one of `--tag`, `--branch`, or `--rev`, or use the `--raw-sources` flag.
|
||||||
|
"###);
|
||||||
|
|
||||||
|
uv_snapshot!(context.filters(), context
|
||||||
|
.add(&["uv-public-pypackage @ git+https://github.com/astral-test/uv-public-pypackage"])
|
||||||
|
.arg("--tag=0.0.1")
|
||||||
|
.arg("--preview")
|
||||||
|
.arg("--script")
|
||||||
|
.arg("script.py"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
"###);
|
||||||
|
|
||||||
|
let script_content = fs_err::read_to_string(context.temp_dir.join("script.py"))?;
|
||||||
|
|
||||||
|
insta::with_settings!({
|
||||||
|
filters => context.filters(),
|
||||||
|
}, {
|
||||||
|
assert_snapshot!(
|
||||||
|
script_content, @r###"
|
||||||
|
# /// script
|
||||||
|
# requires-python = ">=3.11"
|
||||||
|
# dependencies = [
|
||||||
|
# "rich",
|
||||||
|
# "uv-public-pypackage",
|
||||||
|
# ]
|
||||||
|
|
||||||
|
# [tool.uv.sources]
|
||||||
|
# uv-public-pypackage = { git = "https://github.com/astral-test/uv-public-pypackage", tag = "0.0.1" }
|
||||||
|
# ///
|
||||||
|
|
||||||
|
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])
|
||||||
|
"###
|
||||||
|
);
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
|
@ -711,6 +711,10 @@ uv add [OPTIONS] <REQUIREMENTS>...
|
||||||
</ul>
|
</ul>
|
||||||
</dd><dt><code>--rev</code> <i>rev</i></dt><dd><p>Commit to use when adding a dependency from Git</p>
|
</dd><dt><code>--rev</code> <i>rev</i></dt><dd><p>Commit to use when adding a dependency from Git</p>
|
||||||
|
|
||||||
|
</dd><dt><code>--script</code> <i>script</i></dt><dd><p>Add the dependency to the specified Python script, rather than to a project.</p>
|
||||||
|
|
||||||
|
<p>If provided, uv will add the dependency to the script’s inline metadata table, in adhere with PEP 723. If no such inline metadata table is present, a new one will be created and added to the script. When executed via <code>uv run</code>, uv will create a temporary environment for the script with all inline dependencies installed.</p>
|
||||||
|
|
||||||
</dd><dt><code>--tag</code> <i>tag</i></dt><dd><p>Tag to use when adding a dependency from Git</p>
|
</dd><dt><code>--tag</code> <i>tag</i></dt><dd><p>Tag to use when adding a dependency from Git</p>
|
||||||
|
|
||||||
</dd><dt><code>--upgrade</code>, <code>-U</code></dt><dd><p>Allow package upgrades, ignoring pinned versions in any existing output file. Implies <code>--refresh</code></p>
|
</dd><dt><code>--upgrade</code>, <code>-U</code></dt><dd><p>Allow package upgrades, ignoring pinned versions in any existing output file. Implies <code>--refresh</code></p>
|
||||||
|
@ -967,6 +971,10 @@ uv remove [OPTIONS] <PACKAGES>...
|
||||||
|
|
||||||
<li><code>lowest-direct</code>: Resolve the lowest compatible version of any direct dependencies, and the highest compatible version of any transitive dependencies</li>
|
<li><code>lowest-direct</code>: Resolve the lowest compatible version of any direct dependencies, and the highest compatible version of any transitive dependencies</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
</dd><dt><code>--script</code> <i>script</i></dt><dd><p>Remove the dependency from the specified Python script, rather than from a project.</p>
|
||||||
|
|
||||||
|
<p>If provided, uv will remove the dependency from the script’s inline metadata table, in adhere with PEP 723.</p>
|
||||||
|
|
||||||
</dd><dt><code>--upgrade</code>, <code>-U</code></dt><dd><p>Allow package upgrades, ignoring pinned versions in any existing output file. Implies <code>--refresh</code></p>
|
</dd><dt><code>--upgrade</code>, <code>-U</code></dt><dd><p>Allow package upgrades, ignoring pinned versions in any existing output file. Implies <code>--refresh</code></p>
|
||||||
|
|
||||||
</dd><dt><code>--upgrade-package</code>, <code>-P</code> <i>upgrade-package</i></dt><dd><p>Allow upgrades for a specific package, ignoring pinned versions in any existing output file. Implies <code>--refresh-package</code></p>
|
</dd><dt><code>--upgrade-package</code>, <code>-P</code> <i>upgrade-package</i></dt><dd><p>Allow upgrades for a specific package, ignoring pinned versions in any existing output file. Implies <code>--refresh-package</code></p>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue