mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-04 19:08:04 +00:00
5610 lines
168 KiB
Rust
5610 lines
168 KiB
Rust
#![cfg(all(feature = "python", feature = "pypi"))]
|
||
|
||
use std::process::Command;
|
||
|
||
use anyhow::Result;
|
||
use assert_cmd::prelude::*;
|
||
use assert_fs::prelude::*;
|
||
use base64::{prelude::BASE64_STANDARD as base64, Engine};
|
||
use indoc::indoc;
|
||
use itertools::Itertools;
|
||
use url::Url;
|
||
|
||
use common::{uv_snapshot, TestContext};
|
||
use uv_fs::Simplified;
|
||
|
||
use crate::common::{get_bin, venv_bin_path, BUILD_VENDOR_LINKS_URL};
|
||
|
||
mod common;
|
||
|
||
// This is a fine-grained token that only has read-only access to the `uv-private-pypackage` repository
|
||
const READ_ONLY_GITHUB_TOKEN: &[&str] = &[
|
||
"Z2l0aHViX3BhdA==",
|
||
"MTFCR0laQTdRMGdXeGsweHV6ekR2Mg==",
|
||
"NVZMaExzZmtFMHZ1ZEVNd0pPZXZkV040WUdTcmk2WXREeFB4TFlybGlwRTZONEpHV01FMnFZQWJVUm4=",
|
||
];
|
||
|
||
// This is a fine-grained token that only has read-only access to the `uv-private-pypackage-2` repository
|
||
#[cfg(not(windows))]
|
||
const READ_ONLY_GITHUB_TOKEN_2: &[&str] = &[
|
||
"Z2l0aHViX3BhdA==",
|
||
"MTFCR0laQTdRMHV1MEpwaFp4dFFyRwo=",
|
||
"cnNmNXJwMHk2WWpteVZvb2ZFc0c5WUs5b2NPcFY1aVpYTnNmdE05eEhaM0lGSExSSktDWTcxeVBVZXkK",
|
||
];
|
||
|
||
/// Decode a split, base64 encoded authentication token.
|
||
/// We split and encode the token to bypass revoke by GitHub's secret scanning
|
||
fn decode_token(content: &[&str]) -> String {
|
||
let token = content
|
||
.iter()
|
||
.map(|part| base64.decode(part).unwrap())
|
||
.map(|decoded| {
|
||
std::str::from_utf8(decoded.as_slice())
|
||
.unwrap()
|
||
.trim_end()
|
||
.to_string()
|
||
})
|
||
.join("_");
|
||
token
|
||
}
|
||
|
||
/// Create a `pip uninstall` command with options shared across scenarios.
|
||
fn uninstall_command(context: &TestContext) -> Command {
|
||
let mut command = Command::new(get_bin());
|
||
command
|
||
.arg("pip")
|
||
.arg("uninstall")
|
||
.arg("--cache-dir")
|
||
.arg(context.cache_dir.path())
|
||
.env("VIRTUAL_ENV", context.venv.as_os_str())
|
||
.current_dir(&context.temp_dir);
|
||
|
||
if cfg!(all(windows, debug_assertions)) {
|
||
// TODO(konstin): Reduce stack usage in debug mode enough that the tests pass with the
|
||
// default windows stack of 1MB
|
||
command.env("UV_STACK_SIZE", (2 * 1024 * 1024).to_string());
|
||
}
|
||
|
||
command
|
||
}
|
||
|
||
#[test]
|
||
fn missing_requirements_txt() {
|
||
let context = TestContext::new("3.12");
|
||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg("-r")
|
||
.arg("requirements.txt")
|
||
.arg("--strict"), @r###"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: File not found: `requirements.txt`
|
||
"###
|
||
);
|
||
|
||
requirements_txt.assert(predicates::path::missing());
|
||
}
|
||
|
||
#[test]
|
||
fn empty_requirements_txt() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||
requirements_txt.touch()?;
|
||
|
||
uv_snapshot!(context.install()
|
||
.arg("-r")
|
||
.arg("requirements.txt")
|
||
.arg("--strict"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
warning: Requirements file requirements.txt does not contain any dependencies
|
||
Audited 0 packages in [TIME]
|
||
"###
|
||
);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn missing_pyproject_toml() {
|
||
let context = TestContext::new("3.12");
|
||
|
||
uv_snapshot!(context.install()
|
||
.arg("-r")
|
||
.arg("pyproject.toml"), @r###"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: File not found: `pyproject.toml`
|
||
"###
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn invalid_pyproject_toml_syntax() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||
pyproject_toml.write_str("123 - 456")?;
|
||
|
||
uv_snapshot!(context.install()
|
||
.arg("-r")
|
||
.arg("pyproject.toml"), @r###"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: Failed to parse: `pyproject.toml`
|
||
Caused by: TOML parse error at line 1, column 5
|
||
|
|
||
1 | 123 - 456
|
||
| ^
|
||
expected `.`, `=`
|
||
|
||
"###
|
||
);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn invalid_pyproject_toml_schema() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||
pyproject_toml.write_str("[project]")?;
|
||
|
||
uv_snapshot!(context.install()
|
||
.arg("-r")
|
||
.arg("pyproject.toml"), @r###"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: Failed to parse: `pyproject.toml`
|
||
Caused by: TOML parse error at line 1, column 1
|
||
|
|
||
1 | [project]
|
||
| ^^^^^^^^^
|
||
missing field `name`
|
||
|
||
"###
|
||
);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// For indirect, non-user controlled pyproject.toml, we don't enforce correctness.
|
||
///
|
||
/// If we fail to extract the PEP 621 metadata, we fall back to treating it as a source
|
||
/// tree, as there are some cases where the `pyproject.toml` may not be a valid PEP
|
||
/// 621 file, but might still resolve under PEP 517. (If the source tree doesn't
|
||
/// resolve under PEP 517, we'll catch that later.)
|
||
///
|
||
/// For example, Hatch's "Context formatting" API is not compliant with PEP 621, as
|
||
/// it expects dynamic processing by the build backend for the static metadata
|
||
/// fields. See: <https://hatch.pypa.io/latest/config/context/>
|
||
#[test]
|
||
fn invalid_pyproject_toml_requirement_indirect() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
let pyproject_toml = context.temp_dir.child("path_dep/pyproject.toml");
|
||
pyproject_toml.write_str(
|
||
r#"[project]
|
||
name = "project"
|
||
dependencies = ["flask==1.0.x"]
|
||
"#,
|
||
)?;
|
||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||
requirements_txt.write_str("./path_dep")?;
|
||
|
||
let filters = [("exit status", "exit code")]
|
||
.into_iter()
|
||
.chain(context.filters())
|
||
.collect::<Vec<_>>();
|
||
|
||
uv_snapshot!(filters, context.install()
|
||
.arg("-r")
|
||
.arg("requirements.txt"), @r###"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: Failed to download and build: `project @ file://[TEMP_DIR]/path_dep`
|
||
Caused by: Failed to build: `project @ file://[TEMP_DIR]/path_dep`
|
||
Caused by: Build backend failed to determine extra requires with `build_wheel()` with exit code: 1
|
||
--- stdout:
|
||
configuration error: `project.dependencies[0]` must be pep508
|
||
DESCRIPTION:
|
||
Project dependency specification according to PEP 508
|
||
|
||
GIVEN VALUE:
|
||
"flask==1.0.x"
|
||
|
||
OFFENDING RULE: 'format'
|
||
|
||
DEFINITION:
|
||
{
|
||
"$id": "#/definitions/dependency",
|
||
"title": "Dependency",
|
||
"type": "string",
|
||
"format": "pep508"
|
||
}
|
||
--- stderr:
|
||
Traceback (most recent call last):
|
||
File "<string>", line 14, in <module>
|
||
File "[CACHE_DIR]/environments-v0/[TMP]/build_meta.py", line 325, in get_requires_for_build_wheel
|
||
return self._get_build_requires(config_settings, requirements=['wheel'])
|
||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||
File "[CACHE_DIR]/environments-v0/[TMP]/build_meta.py", line 295, in _get_build_requires
|
||
self.run_setup()
|
||
File "[CACHE_DIR]/environments-v0/[TMP]/build_meta.py", line 487, in run_setup
|
||
super().run_setup(setup_script=setup_script)
|
||
File "[CACHE_DIR]/environments-v0/[TMP]/build_meta.py", line 311, in run_setup
|
||
exec(code, locals())
|
||
File "<string>", line 1, in <module>
|
||
File "[CACHE_DIR]/environments-v0/[TMP]/__init__.py", line 104, in setup
|
||
return distutils.core.setup(**attrs)
|
||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||
File "[CACHE_DIR]/environments-v0/[TMP]/core.py", line 159, in setup
|
||
dist.parse_config_files()
|
||
File "[CACHE_DIR]/environments-v0/[TMP]/_virtualenv.py", line 22, in parse_config_files
|
||
result = old_parse_config_files(self, *args, **kwargs)
|
||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||
File "[CACHE_DIR]/environments-v0/[TMP]/dist.py", line 631, in parse_config_files
|
||
pyprojecttoml.apply_configuration(self, filename, ignore_option_errors)
|
||
File "[CACHE_DIR]/environments-v0/[TMP]/pyprojecttoml.py", line 68, in apply_configuration
|
||
config = read_configuration(filepath, True, ignore_option_errors, dist)
|
||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||
File "[CACHE_DIR]/environments-v0/[TMP]/pyprojecttoml.py", line 129, in read_configuration
|
||
validate(subset, filepath)
|
||
File "[CACHE_DIR]/environments-v0/[TMP]/pyprojecttoml.py", line 57, in validate
|
||
raise ValueError(f"{error}/n{summary}") from None
|
||
ValueError: invalid pyproject.toml config: `project.dependencies[0]`.
|
||
configuration error: `project.dependencies[0]` must be pep508
|
||
---
|
||
"###
|
||
);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn missing_pip() {
|
||
uv_snapshot!(Command::new(get_bin()).arg("install"), @r###"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: unrecognized subcommand 'install'
|
||
|
||
tip: a similar subcommand exists: 'uv pip install'
|
||
|
||
Usage: uv [OPTIONS] <COMMAND>
|
||
|
||
For more information, try '--help'.
|
||
"###);
|
||
}
|
||
|
||
#[test]
|
||
fn no_solution() {
|
||
let context = TestContext::new("3.12");
|
||
|
||
uv_snapshot!(context.install()
|
||
.arg("flask>=3.0.2")
|
||
.arg("WerkZeug<1.0.0")
|
||
.arg("--strict"), @r###"
|
||
success: false
|
||
exit_code: 1
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
× No solution found when resolving dependencies:
|
||
╰─▶ Because only flask<=3.0.2 is available and flask==3.0.2 depends on werkzeug>=3.0.0, we can conclude that flask>=3.0.2 depends on werkzeug>=3.0.0.
|
||
And because you require flask>=3.0.2 and werkzeug<1.0.0, we can conclude that the requirements are unsatisfiable.
|
||
"###);
|
||
}
|
||
|
||
/// Install a package from the command line into a virtual environment.
|
||
#[test]
|
||
fn install_package() {
|
||
let context = TestContext::new("3.12");
|
||
|
||
// Install Flask.
|
||
uv_snapshot!(context.install()
|
||
.arg("Flask")
|
||
.arg("--strict"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 7 packages in [TIME]
|
||
Prepared 7 packages in [TIME]
|
||
Installed 7 packages in [TIME]
|
||
+ blinker==1.7.0
|
||
+ click==8.1.7
|
||
+ flask==3.0.2
|
||
+ itsdangerous==2.1.2
|
||
+ jinja2==3.1.3
|
||
+ markupsafe==2.1.5
|
||
+ werkzeug==3.0.1
|
||
"###
|
||
);
|
||
|
||
context.assert_command("import flask").success();
|
||
}
|
||
|
||
/// Install a package from a `requirements.txt` into a virtual environment.
|
||
#[test]
|
||
fn install_requirements_txt() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
// Install Flask.
|
||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||
requirements_txt.write_str("Flask")?;
|
||
|
||
uv_snapshot!(context.install()
|
||
.arg("-r")
|
||
.arg("requirements.txt")
|
||
.arg("--strict"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 7 packages in [TIME]
|
||
Prepared 7 packages in [TIME]
|
||
Installed 7 packages in [TIME]
|
||
+ blinker==1.7.0
|
||
+ click==8.1.7
|
||
+ flask==3.0.2
|
||
+ itsdangerous==2.1.2
|
||
+ jinja2==3.1.3
|
||
+ markupsafe==2.1.5
|
||
+ werkzeug==3.0.1
|
||
"###
|
||
);
|
||
|
||
context.assert_command("import flask").success();
|
||
|
||
// Install Jinja2 (which should already be installed, but shouldn't remove other packages).
|
||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||
requirements_txt.write_str("Jinja2")?;
|
||
|
||
uv_snapshot!(context.install()
|
||
.arg("-r")
|
||
.arg("requirements.txt")
|
||
.arg("--strict"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Audited 1 package in [TIME]
|
||
"###
|
||
);
|
||
|
||
context.assert_command("import flask").success();
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Install a requirements file with pins that conflict
|
||
///
|
||
/// This is likely to occur in the real world when compiled on one platform then installed on another.
|
||
#[test]
|
||
fn install_requirements_txt_conflicting_pins() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||
|
||
// We pin `click` to a conflicting requirement
|
||
requirements_txt.write_str(
|
||
r"
|
||
blinker==1.7.0
|
||
click==7.0.0
|
||
flask==3.0.2
|
||
itsdangerous==2.1.2
|
||
jinja2==3.1.3
|
||
markupsafe==2.1.5
|
||
werkzeug==3.0.1
|
||
",
|
||
)?;
|
||
|
||
uv_snapshot!(context.install()
|
||
.arg("-r")
|
||
.arg("requirements.txt")
|
||
.arg("--strict"), @r###"
|
||
success: false
|
||
exit_code: 1
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
× No solution found when resolving dependencies:
|
||
╰─▶ Because flask==3.0.2 depends on click>=8.1.3 and you require click==7.0.0, we can conclude that your requirements and flask==3.0.2 are incompatible.
|
||
And because you require flask==3.0.2, we can conclude that the requirements are unsatisfiable.
|
||
"###
|
||
);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Install a `pyproject.toml` file with a `poetry` section.
|
||
#[test]
|
||
fn install_pyproject_toml_poetry() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||
pyproject_toml.write_str(
|
||
r#"[tool.poetry]
|
||
name = "poetry-editable"
|
||
version = "0.1.0"
|
||
description = ""
|
||
authors = ["Astral Software Inc. <hey@astral.sh>"]
|
||
|
||
[tool.poetry.dependencies]
|
||
python = "^3.10"
|
||
anyio = "^3"
|
||
iniconfig = { version = "*", optional = true }
|
||
|
||
[tool.poetry.extras]
|
||
test = ["iniconfig"]
|
||
|
||
[build-system]
|
||
requires = ["poetry-core"]
|
||
build-backend = "poetry.core.masonry.api"
|
||
"#,
|
||
)?;
|
||
|
||
uv_snapshot!(context.install()
|
||
.arg("-r")
|
||
.arg("pyproject.toml")
|
||
.arg("--extra")
|
||
.arg("test"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 4 packages in [TIME]
|
||
Prepared 4 packages in [TIME]
|
||
Installed 4 packages in [TIME]
|
||
+ anyio==3.7.1
|
||
+ idna==3.6
|
||
+ iniconfig==2.0.0
|
||
+ sniffio==1.3.1
|
||
"###
|
||
);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Respect installed versions when resolving.
|
||
#[test]
|
||
fn respect_installed_and_reinstall() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
// Install Flask.
|
||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||
requirements_txt.write_str("Flask==2.3.2")?;
|
||
|
||
uv_snapshot!(context.install()
|
||
.arg("-r")
|
||
.arg("requirements.txt")
|
||
.arg("--strict"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 7 packages in [TIME]
|
||
Prepared 7 packages in [TIME]
|
||
Installed 7 packages in [TIME]
|
||
+ blinker==1.7.0
|
||
+ click==8.1.7
|
||
+ flask==2.3.2
|
||
+ itsdangerous==2.1.2
|
||
+ jinja2==3.1.3
|
||
+ markupsafe==2.1.5
|
||
+ werkzeug==3.0.1
|
||
"###
|
||
);
|
||
|
||
context.assert_command("import flask").success();
|
||
|
||
// Re-install Flask. We should respect the existing version.
|
||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||
requirements_txt.write_str("Flask")?;
|
||
|
||
uv_snapshot!(context.install()
|
||
.arg("-r")
|
||
.arg("requirements.txt")
|
||
.arg("--strict"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Audited 1 package in [TIME]
|
||
"###
|
||
);
|
||
|
||
context.assert_command("import flask").success();
|
||
|
||
// Install a newer version of Flask. We should upgrade it.
|
||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||
requirements_txt.write_str("Flask==2.3.3")?;
|
||
|
||
let filters = if cfg!(windows) {
|
||
// Remove the colorama count on windows
|
||
context
|
||
.filters()
|
||
.into_iter()
|
||
.chain([("Resolved 8 packages", "Resolved 7 packages")])
|
||
.collect()
|
||
} else {
|
||
context.filters()
|
||
};
|
||
uv_snapshot!(filters, context.install()
|
||
.arg("-r")
|
||
.arg("requirements.txt")
|
||
.arg("--strict"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 7 packages in [TIME]
|
||
Prepared 1 package in [TIME]
|
||
Uninstalled 1 package in [TIME]
|
||
Installed 1 package in [TIME]
|
||
- flask==2.3.2
|
||
+ flask==2.3.3
|
||
"###
|
||
);
|
||
|
||
// Re-install Flask. We should upgrade it.
|
||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||
requirements_txt.write_str("Flask")?;
|
||
|
||
uv_snapshot!(filters, context.install()
|
||
.arg("-r")
|
||
.arg("requirements.txt")
|
||
.arg("--reinstall-package")
|
||
.arg("Flask")
|
||
.arg("--strict"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 7 packages in [TIME]
|
||
Prepared 1 package in [TIME]
|
||
Uninstalled 1 package in [TIME]
|
||
Installed 1 package in [TIME]
|
||
- flask==2.3.3
|
||
+ flask==3.0.2
|
||
"###
|
||
);
|
||
|
||
// Re-install Flask. We should install even though the version is current
|
||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||
requirements_txt.write_str("Flask")?;
|
||
|
||
uv_snapshot!(filters, context.install()
|
||
.arg("-r")
|
||
.arg("requirements.txt")
|
||
.arg("--reinstall-package")
|
||
.arg("Flask")
|
||
.arg("--strict"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 7 packages in [TIME]
|
||
Uninstalled 1 package in [TIME]
|
||
Installed 1 package in [TIME]
|
||
- flask==3.0.2
|
||
+ flask==3.0.2
|
||
"###
|
||
);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Respect installed versions when resolving.
|
||
#[test]
|
||
fn reinstall_extras() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
// Install httpx.
|
||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||
requirements_txt.write_str("httpx")?;
|
||
|
||
uv_snapshot!(context.install()
|
||
.arg("-r")
|
||
.arg("requirements.txt")
|
||
.arg("--strict"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 7 packages in [TIME]
|
||
Prepared 7 packages in [TIME]
|
||
Installed 7 packages in [TIME]
|
||
+ anyio==4.3.0
|
||
+ certifi==2024.2.2
|
||
+ h11==0.14.0
|
||
+ httpcore==1.0.4
|
||
+ httpx==0.27.0
|
||
+ idna==3.6
|
||
+ sniffio==1.3.1
|
||
"###
|
||
);
|
||
|
||
context.assert_command("import httpx").success();
|
||
|
||
// Re-install httpx, with an extra.
|
||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||
requirements_txt.write_str("httpx[http2]")?;
|
||
|
||
uv_snapshot!(context.install()
|
||
.arg("-r")
|
||
.arg("requirements.txt")
|
||
.arg("--strict"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 10 packages in [TIME]
|
||
Prepared 3 packages in [TIME]
|
||
Installed 3 packages in [TIME]
|
||
+ h2==4.1.0
|
||
+ hpack==4.0.0
|
||
+ hyperframe==6.0.1
|
||
"###
|
||
);
|
||
|
||
context.assert_command("import httpx").success();
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Warn, but don't fail, when uninstalling incomplete packages.
|
||
#[test]
|
||
fn reinstall_incomplete() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
// Install anyio.
|
||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||
requirements_txt.write_str("anyio==3.7.0")?;
|
||
|
||
uv_snapshot!(context.install()
|
||
.arg("-r")
|
||
.arg("requirements.txt"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 3 packages in [TIME]
|
||
Prepared 3 packages in [TIME]
|
||
Installed 3 packages in [TIME]
|
||
+ anyio==3.7.0
|
||
+ idna==3.6
|
||
+ sniffio==1.3.1
|
||
"###
|
||
);
|
||
|
||
// Manually remove the `RECORD` file.
|
||
fs_err::remove_file(context.site_packages().join("anyio-3.7.0.dist-info/RECORD"))?;
|
||
|
||
// Re-install anyio.
|
||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||
requirements_txt.write_str("anyio==4.0.0")?;
|
||
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg("-r")
|
||
.arg("requirements.txt"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 3 packages in [TIME]
|
||
Prepared 1 package in [TIME]
|
||
warning: Failed to uninstall package at [SITE_PACKAGES]/anyio-3.7.0.dist-info due to missing RECORD file. Installation may result in an incomplete environment.
|
||
Uninstalled 1 package in [TIME]
|
||
Installed 1 package in [TIME]
|
||
- anyio==3.7.0
|
||
+ anyio==4.0.0
|
||
"###
|
||
);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Like `pip`, we (unfortunately) allow incompatible environments.
|
||
#[test]
|
||
fn allow_incompatibilities() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
// Install Flask, which relies on `Werkzeug>=3.0.0`.
|
||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||
requirements_txt.write_str("Flask")?;
|
||
|
||
uv_snapshot!(context.install()
|
||
.arg("-r")
|
||
.arg("requirements.txt")
|
||
.arg("--strict"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 7 packages in [TIME]
|
||
Prepared 7 packages in [TIME]
|
||
Installed 7 packages in [TIME]
|
||
+ blinker==1.7.0
|
||
+ click==8.1.7
|
||
+ flask==3.0.2
|
||
+ itsdangerous==2.1.2
|
||
+ jinja2==3.1.3
|
||
+ markupsafe==2.1.5
|
||
+ werkzeug==3.0.1
|
||
"###
|
||
);
|
||
|
||
context.assert_command("import flask").success();
|
||
|
||
// Install an incompatible version of Jinja2.
|
||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||
requirements_txt.write_str("jinja2==2.11.3")?;
|
||
|
||
uv_snapshot!(context.install()
|
||
.arg("-r")
|
||
.arg("requirements.txt")
|
||
.arg("--strict"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 2 packages in [TIME]
|
||
Prepared 1 package in [TIME]
|
||
Uninstalled 1 package in [TIME]
|
||
Installed 1 package in [TIME]
|
||
- jinja2==3.1.3
|
||
+ jinja2==2.11.3
|
||
warning: The package `flask` requires `jinja2>=3.1.2`, but `2.11.3` is installed.
|
||
"###
|
||
);
|
||
|
||
// This no longer works, since we have an incompatible version of Jinja2.
|
||
context.assert_command("import flask").failure();
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn install_editable() {
|
||
let context = TestContext::new("3.12");
|
||
|
||
// Install the editable package.
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg("-e")
|
||
.arg(context.workspace_root.join("scripts/packages/poetry_editable")), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 4 packages in [TIME]
|
||
Prepared 4 packages in [TIME]
|
||
Installed 4 packages in [TIME]
|
||
+ anyio==4.3.0
|
||
+ idna==3.6
|
||
+ poetry-editable==0.1.0 (from file://[WORKSPACE]/scripts/packages/poetry_editable)
|
||
+ sniffio==1.3.1
|
||
"###
|
||
);
|
||
|
||
// Install it again (no-op).
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg("-e")
|
||
.arg(context.workspace_root.join("scripts/packages/poetry_editable")), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Audited 1 package in [TIME]
|
||
"###
|
||
);
|
||
|
||
// Add another, non-editable dependency.
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg("-e")
|
||
.arg(context.workspace_root.join("scripts/packages/poetry_editable"))
|
||
.arg("black"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 10 packages in [TIME]
|
||
Prepared 6 packages in [TIME]
|
||
Installed 6 packages in [TIME]
|
||
+ black==24.3.0
|
||
+ click==8.1.7
|
||
+ mypy-extensions==1.0.0
|
||
+ packaging==24.0
|
||
+ pathspec==0.12.1
|
||
+ platformdirs==4.2.0
|
||
"###
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn install_editable_and_registry() {
|
||
let context = TestContext::new("3.12");
|
||
|
||
// Install the registry-based version of Black.
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg("black"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 6 packages in [TIME]
|
||
Prepared 6 packages in [TIME]
|
||
Installed 6 packages in [TIME]
|
||
+ black==24.3.0
|
||
+ click==8.1.7
|
||
+ mypy-extensions==1.0.0
|
||
+ packaging==24.0
|
||
+ pathspec==0.12.1
|
||
+ platformdirs==4.2.0
|
||
"###
|
||
);
|
||
|
||
// Install the editable version of Black. This should remove the registry-based version.
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg("-e")
|
||
.arg(context.workspace_root.join("scripts/packages/black_editable")), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 1 package in [TIME]
|
||
Prepared 1 package in [TIME]
|
||
Uninstalled 1 package in [TIME]
|
||
Installed 1 package in [TIME]
|
||
- black==24.3.0
|
||
+ black==0.1.0 (from file://[WORKSPACE]/scripts/packages/black_editable)
|
||
"###
|
||
);
|
||
|
||
// Re-install the registry-based version of Black. This should be a no-op, since we have a
|
||
// version of Black installed (the editable version) that satisfies the requirements.
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg("black")
|
||
.arg("--strict"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Audited 1 package in [TIME]
|
||
"###
|
||
);
|
||
|
||
let filters: Vec<_> = context
|
||
.filters()
|
||
.into_iter()
|
||
.chain([
|
||
// Remove colorama
|
||
("Resolved 7 packages", "Resolved 6 packages"),
|
||
])
|
||
.collect();
|
||
|
||
// Re-install Black at a specific version. This should replace the editable version.
|
||
uv_snapshot!(filters, context.install()
|
||
.arg("black==23.10.0"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 6 packages in [TIME]
|
||
Prepared 1 package in [TIME]
|
||
Uninstalled 1 package in [TIME]
|
||
Installed 1 package in [TIME]
|
||
- black==0.1.0 (from file://[WORKSPACE]/scripts/packages/black_editable)
|
||
+ black==23.10.0
|
||
"###
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn install_editable_no_binary() {
|
||
let context = TestContext::new("3.12");
|
||
|
||
// Install the editable package with no-binary enabled
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg("-e")
|
||
.arg(context.workspace_root.join("scripts/packages/black_editable"))
|
||
.arg("--no-binary")
|
||
.arg(":all:"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 1 package in [TIME]
|
||
Prepared 1 package in [TIME]
|
||
Installed 1 package in [TIME]
|
||
+ black==0.1.0 (from file://[WORKSPACE]/scripts/packages/black_editable)
|
||
"###
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn install_editable_compatible_constraint() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
let constraints_txt = context.temp_dir.child("constraints.txt");
|
||
constraints_txt.write_str("black==0.1.0")?;
|
||
|
||
// Install the editable package with a compatible constraint.
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg("-e")
|
||
.arg(context.workspace_root.join("scripts/packages/black_editable"))
|
||
.arg("--constraint")
|
||
.arg("constraints.txt"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 1 package in [TIME]
|
||
Prepared 1 package in [TIME]
|
||
Installed 1 package in [TIME]
|
||
+ black==0.1.0 (from file://[WORKSPACE]/scripts/packages/black_editable)
|
||
"###
|
||
);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn install_editable_incompatible_constraint_version() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
let constraints_txt = context.temp_dir.child("constraints.txt");
|
||
constraints_txt.write_str("black>0.1.0")?;
|
||
|
||
// Install the editable package with an incompatible constraint.
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg("-e")
|
||
.arg(context.workspace_root.join("scripts/packages/black_editable"))
|
||
.arg("--constraint")
|
||
.arg("constraints.txt"), @r###"
|
||
success: false
|
||
exit_code: 1
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
× No solution found when resolving dependencies:
|
||
╰─▶ Because only black<=0.1.0 is available and you require black>0.1.0, we can conclude that the requirements are unsatisfiable.
|
||
"###
|
||
);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn install_editable_incompatible_constraint_url() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
let constraints_txt = context.temp_dir.child("constraints.txt");
|
||
constraints_txt.write_str("black @ https://files.pythonhosted.org/packages/0f/89/294c9a6b6c75a08da55e9d05321d0707e9418735e3062b12ef0f54c33474/black-24.4.2-py3-none-any.whl")?;
|
||
|
||
// Install the editable package with an incompatible constraint.
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg("-e")
|
||
.arg(context.workspace_root.join("scripts/packages/black_editable"))
|
||
.arg("--constraint")
|
||
.arg("constraints.txt"), @r###"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: Requirements contain conflicting URLs for package `black`:
|
||
- [WORKSPACE]/scripts/packages/black_editable
|
||
- https://files.pythonhosted.org/packages/0f/89/294c9a6b6c75a08da55e9d05321d0707e9418735e3062b12ef0f54c33474/black-24.4.2-py3-none-any.whl
|
||
"###
|
||
);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn install_editable_pep_508_requirements_txt() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||
requirements_txt.write_str(&indoc::formatdoc! {r"
|
||
-e black[d] @ file://{workspace_root}/scripts/packages/black_editable
|
||
",
|
||
workspace_root = context.workspace_root.simplified_display(),
|
||
})?;
|
||
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg("-r")
|
||
.arg("requirements.txt"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 8 packages in [TIME]
|
||
Prepared 8 packages in [TIME]
|
||
Installed 8 packages in [TIME]
|
||
+ aiohttp==3.9.3
|
||
+ aiosignal==1.3.1
|
||
+ attrs==23.2.0
|
||
+ black==0.1.0 (from file://[WORKSPACE]/scripts/packages/black_editable)
|
||
+ frozenlist==1.4.1
|
||
+ idna==3.6
|
||
+ multidict==6.0.5
|
||
+ yarl==1.9.4
|
||
"###
|
||
);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn install_editable_pep_508_cli() {
|
||
let context = TestContext::new("3.12");
|
||
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg("-e")
|
||
.arg(format!("black[d] @ file://{workspace_root}/scripts/packages/black_editable", workspace_root = context.workspace_root.simplified_display())), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 8 packages in [TIME]
|
||
Prepared 8 packages in [TIME]
|
||
Installed 8 packages in [TIME]
|
||
+ aiohttp==3.9.3
|
||
+ aiosignal==1.3.1
|
||
+ attrs==23.2.0
|
||
+ black==0.1.0 (from file://[WORKSPACE]/scripts/packages/black_editable)
|
||
+ frozenlist==1.4.1
|
||
+ idna==3.6
|
||
+ multidict==6.0.5
|
||
+ yarl==1.9.4
|
||
"###
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn install_editable_bare_cli() {
|
||
let context = TestContext::new("3.12");
|
||
|
||
let packages_dir = context.workspace_root.join("scripts/packages");
|
||
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg("-e")
|
||
.arg("black_editable")
|
||
.current_dir(&packages_dir), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 1 package in [TIME]
|
||
Prepared 1 package in [TIME]
|
||
Installed 1 package in [TIME]
|
||
+ black==0.1.0 (from file://[WORKSPACE]/scripts/packages/black_editable)
|
||
"###
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn install_editable_bare_requirements_txt() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||
requirements_txt.write_str("-e black_editable")?;
|
||
|
||
let packages_dir = context.workspace_root.join("scripts/packages");
|
||
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg("-r")
|
||
.arg(requirements_txt.path())
|
||
.current_dir(&packages_dir), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 1 package in [TIME]
|
||
Prepared 1 package in [TIME]
|
||
Installed 1 package in [TIME]
|
||
+ black==0.1.0 (from file://[WORKSPACE]/scripts/packages/black_editable)
|
||
"###
|
||
);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn invalid_editable_no_url() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||
requirements_txt.write_str("-e black==0.1.0")?;
|
||
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg("-r")
|
||
.arg("requirements.txt"), @r###"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: Unsupported editable requirement in `requirements.txt`
|
||
Caused by: Editable `black` must refer to a local directory, not a versioned package
|
||
"###
|
||
);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn invalid_editable_unnamed_https_url() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||
requirements_txt.write_str("-e https://files.pythonhosted.org/packages/0f/89/294c9a6b6c75a08da55e9d05321d0707e9418735e3062b12ef0f54c33474/black-24.4.2-py3-none-any.whl")?;
|
||
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg("-r")
|
||
.arg("requirements.txt"), @r###"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: Unsupported editable requirement in `requirements.txt`
|
||
Caused by: Editable must refer to a local directory, not an HTTPS URL: `https://files.pythonhosted.org/packages/0f/89/294c9a6b6c75a08da55e9d05321d0707e9418735e3062b12ef0f54c33474/black-24.4.2-py3-none-any.whl`
|
||
"###
|
||
);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn invalid_editable_named_https_url() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||
requirements_txt.write_str("-e black @ https://files.pythonhosted.org/packages/0f/89/294c9a6b6c75a08da55e9d05321d0707e9418735e3062b12ef0f54c33474/black-24.4.2-py3-none-any.whl")?;
|
||
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg("-r")
|
||
.arg("requirements.txt"), @r###"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: Unsupported editable requirement in `requirements.txt`
|
||
Caused by: Editable `black` must refer to a local directory, not an HTTPS URL: `https://files.pythonhosted.org/packages/0f/89/294c9a6b6c75a08da55e9d05321d0707e9418735e3062b12ef0f54c33474/black-24.4.2-py3-none-any.whl`
|
||
"###
|
||
);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Install a source distribution that uses the `flit` build system, along with `flit`
|
||
/// at the top-level, along with `--reinstall` to force a re-download after resolution, to ensure
|
||
/// that the `flit` install and the source distribution build don't conflict.
|
||
#[test]
|
||
fn reinstall_build_system() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||
requirements_txt.write_str(indoc! {r"
|
||
flit_core<4.0.0
|
||
flask @ https://files.pythonhosted.org/packages/d8/09/c1a7354d3925a3c6c8cfdebf4245bae67d633ffda1ba415add06ffc839c5/flask-3.0.0.tar.gz
|
||
"
|
||
})?;
|
||
|
||
uv_snapshot!(context.install()
|
||
.arg("--reinstall")
|
||
.arg("-r")
|
||
.arg("requirements.txt")
|
||
.arg("--strict"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 8 packages in [TIME]
|
||
Prepared 8 packages in [TIME]
|
||
Installed 8 packages in [TIME]
|
||
+ blinker==1.7.0
|
||
+ click==8.1.7
|
||
+ flask==3.0.0 (from https://files.pythonhosted.org/packages/d8/09/c1a7354d3925a3c6c8cfdebf4245bae67d633ffda1ba415add06ffc839c5/flask-3.0.0.tar.gz)
|
||
+ flit-core==3.9.0
|
||
+ itsdangerous==2.1.2
|
||
+ jinja2==3.1.3
|
||
+ markupsafe==2.1.5
|
||
+ werkzeug==3.0.1
|
||
"###
|
||
);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Install a package without using the remote index
|
||
#[test]
|
||
fn install_no_index() {
|
||
let context = TestContext::new("3.12");
|
||
|
||
uv_snapshot!(context.install()
|
||
.arg("Flask")
|
||
.arg("--no-index"), @r###"
|
||
success: false
|
||
exit_code: 1
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
× No solution found when resolving dependencies:
|
||
╰─▶ Because flask was not found in the provided package locations and you require flask, we can conclude that the requirements are unsatisfiable.
|
||
|
||
hint: Packages were unavailable because index lookups were disabled and no additional package locations were provided (try: `--find-links <uri>`)
|
||
"###
|
||
);
|
||
|
||
context.assert_command("import flask").failure();
|
||
}
|
||
|
||
/// Install a package without using the remote index
|
||
/// Covers a case where the user requests a version which should be included in the error
|
||
#[test]
|
||
fn install_no_index_version() {
|
||
let context = TestContext::new("3.12");
|
||
|
||
uv_snapshot!(context.install()
|
||
.arg("Flask==3.0.0")
|
||
.arg("--no-index"), @r###"
|
||
success: false
|
||
exit_code: 1
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
× No solution found when resolving dependencies:
|
||
╰─▶ Because flask was not found in the provided package locations and you require flask==3.0.0, we can conclude that the requirements are unsatisfiable.
|
||
|
||
hint: Packages were unavailable because index lookups were disabled and no additional package locations were provided (try: `--find-links <uri>`)
|
||
"###
|
||
);
|
||
|
||
context.assert_command("import flask").failure();
|
||
}
|
||
|
||
/// Install a package via --extra-index-url.
|
||
///
|
||
/// This is a regression test where previously `uv` would consult test.pypi.org
|
||
/// first, and if the package was found there, `uv` would not look at any other
|
||
/// indexes. We fixed this by flipping the priority order of indexes so that
|
||
/// test.pypi.org becomes the fallback (in this example) and the extra indexes
|
||
/// (regular PyPI) are checked first.
|
||
///
|
||
/// (Neither approach matches `pip`'s behavior, which considers versions of
|
||
/// each package from all indexes. `uv` stops at the first index it finds a
|
||
/// package in.)
|
||
///
|
||
/// Ref: <https://github.com/astral-sh/uv/issues/1600>
|
||
#[test]
|
||
fn install_extra_index_url_has_priority() {
|
||
let context = TestContext::new("3.12");
|
||
|
||
uv_snapshot!(context.install_without_exclude_newer()
|
||
.arg("--index-url")
|
||
.arg("https://test.pypi.org/simple")
|
||
.arg("--extra-index-url")
|
||
.arg("https://pypi.org/simple")
|
||
// This tests what we want because BOTH of the following
|
||
// are true: `black` is on pypi.org and test.pypi.org, AND
|
||
// `black==24.2.0` is on pypi.org and NOT test.pypi.org. So
|
||
// this would previously check for `black` on test.pypi.org,
|
||
// find it, but then not find a compatible version. After
|
||
// the fix, `uv` will check pypi.org first since it is given
|
||
// priority via --extra-index-url.
|
||
.arg("black==24.2.0")
|
||
.arg("--no-deps")
|
||
.arg("--exclude-newer")
|
||
.arg("2024-03-09"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 1 package in [TIME]
|
||
Prepared 1 package in [TIME]
|
||
Installed 1 package in [TIME]
|
||
+ black==24.2.0
|
||
"###
|
||
);
|
||
|
||
context.assert_command("import flask").failure();
|
||
}
|
||
|
||
/// Install a package from a public GitHub repository
|
||
#[test]
|
||
#[cfg(feature = "git")]
|
||
fn install_git_public_https() {
|
||
let context = TestContext::new("3.8");
|
||
|
||
uv_snapshot!(
|
||
context
|
||
.install()
|
||
.arg("uv-public-pypackage @ git+https://github.com/astral-test/uv-public-pypackage"),
|
||
@r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 1 package in [TIME]
|
||
Prepared 1 package in [TIME]
|
||
Installed 1 package in [TIME]
|
||
+ uv-public-pypackage==0.1.0 (from git+https://github.com/astral-test/uv-public-pypackage@b270df1a2fb5d012294e9aaf05e7e0bab1e6a389)
|
||
"###);
|
||
|
||
context.assert_installed("uv_public_pypackage", "0.1.0");
|
||
}
|
||
|
||
/// Install a package from a public GitHub repository at a ref that does not exist
|
||
#[test]
|
||
#[cfg(feature = "git")]
|
||
fn install_git_public_https_missing_branch_or_tag() {
|
||
let context = TestContext::new("3.8");
|
||
|
||
let mut filters = context.filters();
|
||
// Windows does not style the command the same as Unix, so we must omit it from the snapshot
|
||
filters.push(("`git fetch .*`", "`git fetch [...]`"));
|
||
filters.push(("exit status", "exit code"));
|
||
|
||
uv_snapshot!(filters, context.install()
|
||
// 2.0.0 does not exist
|
||
.arg("uv-public-pypackage @ git+https://github.com/astral-test/uv-public-pypackage@2.0.0"), @r###"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: Failed to download and build: `uv-public-pypackage @ git+https://github.com/astral-test/uv-public-pypackage@2.0.0`
|
||
Caused by: Git operation failed
|
||
Caused by: failed to clone into: [CACHE_DIR]/git-v0/db/8dab139913c4b566
|
||
Caused by: failed to fetch branch or tag `2.0.0`
|
||
Caused by: process didn't exit successfully: `git fetch [...]` (exit code: 128)
|
||
--- stderr
|
||
fatal: couldn't find remote ref refs/tags/2.0.0
|
||
|
||
"###);
|
||
}
|
||
|
||
/// Install a package from a public GitHub repository at a ref that does not exist
|
||
#[test]
|
||
#[cfg(feature = "git")]
|
||
fn install_git_public_https_missing_commit() {
|
||
let context = TestContext::new("3.8");
|
||
|
||
let mut filters = context.filters();
|
||
// Windows does not style the command the same as Unix, so we must omit it from the snapshot
|
||
filters.push(("`git fetch .*`", "`git fetch [...]`"));
|
||
filters.push(("exit status", "exit code"));
|
||
|
||
// There are flakes on Windows where this irrelevant error is appended
|
||
filters.push((
|
||
"fatal: unable to write response end packet: Broken pipe\n",
|
||
"",
|
||
));
|
||
|
||
uv_snapshot!(filters, context.install()
|
||
// 2.0.0 does not exist
|
||
.arg("uv-public-pypackage @ git+https://github.com/astral-test/uv-public-pypackage@79a935a7a1a0ad6d0bdf72dce0e16cb0a24a1b3b")
|
||
, @r###"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: Failed to download and build: `uv-public-pypackage @ git+https://github.com/astral-test/uv-public-pypackage@79a935a7a1a0ad6d0bdf72dce0e16cb0a24a1b3b`
|
||
Caused by: Git operation failed
|
||
Caused by: failed to clone into: [CACHE_DIR]/git-v0/db/8dab139913c4b566
|
||
Caused by: failed to fetch commit `79a935a7a1a0ad6d0bdf72dce0e16cb0a24a1b3b`
|
||
Caused by: process didn't exit successfully: `git fetch [...]` (exit code: 128)
|
||
--- stderr
|
||
fatal: remote error: upload-pack: not our ref 79a935a7a1a0ad6d0bdf72dce0e16cb0a24a1b3b
|
||
|
||
"###);
|
||
}
|
||
|
||
/// Install a package from a private GitHub repository using a PAT
|
||
#[test]
|
||
#[cfg(all(not(windows), feature = "git"))]
|
||
fn install_git_private_https_pat() {
|
||
let context = TestContext::new("3.8");
|
||
let token = decode_token(READ_ONLY_GITHUB_TOKEN);
|
||
|
||
let filters: Vec<_> = [(token.as_str(), "***")]
|
||
.into_iter()
|
||
.chain(context.filters())
|
||
.collect();
|
||
|
||
let package = format!(
|
||
"uv-private-pypackage@ git+https://{token}@github.com/astral-test/uv-private-pypackage"
|
||
);
|
||
|
||
uv_snapshot!(filters, context.install().arg(package)
|
||
, @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 1 package in [TIME]
|
||
Prepared 1 package in [TIME]
|
||
Installed 1 package in [TIME]
|
||
+ uv-private-pypackage==0.1.0 (from git+https://***@github.com/astral-test/uv-private-pypackage@d780faf0ac91257d4d5a4f0c5a0e4509608c0071)
|
||
"###);
|
||
|
||
context.assert_installed("uv_private_pypackage", "0.1.0");
|
||
}
|
||
|
||
/// Install a package from a private GitHub repository using a PAT
|
||
/// Include a public GitHub repository too, to ensure that the authentication is not erroneously copied over.
|
||
#[test]
|
||
#[cfg(all(not(windows), feature = "git"))]
|
||
fn install_git_private_https_pat_mixed_with_public() {
|
||
let context = TestContext::new("3.8");
|
||
let token = decode_token(READ_ONLY_GITHUB_TOKEN);
|
||
|
||
let filters: Vec<_> = [(token.as_str(), "***")]
|
||
.into_iter()
|
||
.chain(context.filters())
|
||
.collect();
|
||
|
||
let package = format!(
|
||
"uv-private-pypackage @ git+https://{token}@github.com/astral-test/uv-private-pypackage"
|
||
);
|
||
|
||
uv_snapshot!(filters, context.install().arg(package).arg("uv-public-pypackage @ git+https://github.com/astral-test/uv-public-pypackage"),
|
||
@r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 2 packages in [TIME]
|
||
Prepared 2 packages in [TIME]
|
||
Installed 2 packages in [TIME]
|
||
+ uv-private-pypackage==0.1.0 (from git+https://***@github.com/astral-test/uv-private-pypackage@d780faf0ac91257d4d5a4f0c5a0e4509608c0071)
|
||
+ uv-public-pypackage==0.1.0 (from git+https://github.com/astral-test/uv-public-pypackage@b270df1a2fb5d012294e9aaf05e7e0bab1e6a389)
|
||
"###);
|
||
|
||
context.assert_installed("uv_private_pypackage", "0.1.0");
|
||
}
|
||
|
||
/// Install packages from multiple private GitHub repositories with separate PATS
|
||
#[test]
|
||
#[cfg(all(not(windows), feature = "git"))]
|
||
fn install_git_private_https_multiple_pat() {
|
||
let context = TestContext::new("3.8");
|
||
let token_1 = decode_token(READ_ONLY_GITHUB_TOKEN);
|
||
let token_2 = decode_token(READ_ONLY_GITHUB_TOKEN_2);
|
||
|
||
let filters: Vec<_> = [(token_1.as_str(), "***_1"), (token_2.as_str(), "***_2")]
|
||
.into_iter()
|
||
.chain(context.filters())
|
||
.collect();
|
||
|
||
let package_1 = format!(
|
||
"uv-private-pypackage @ git+https://{token_1}@github.com/astral-test/uv-private-pypackage"
|
||
);
|
||
let package_2 = format!(
|
||
"uv-private-pypackage-2 @ git+https://{token_2}@github.com/astral-test/uv-private-pypackage-2"
|
||
);
|
||
|
||
uv_snapshot!(filters, context.install().arg(package_1).arg(package_2)
|
||
, @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 2 packages in [TIME]
|
||
Prepared 2 packages in [TIME]
|
||
Installed 2 packages in [TIME]
|
||
+ uv-private-pypackage==0.1.0 (from git+https://***_1@github.com/astral-test/uv-private-pypackage@d780faf0ac91257d4d5a4f0c5a0e4509608c0071)
|
||
+ uv-private-pypackage-2==0.1.0 (from git+https://***_2@github.com/astral-test/uv-private-pypackage-2@45c0bec7365710f09b1f4dbca61c86dde9537e4e)
|
||
"###);
|
||
|
||
context.assert_installed("uv_private_pypackage", "0.1.0");
|
||
}
|
||
|
||
/// Install a package from a private GitHub repository at a specific commit using a PAT
|
||
#[test]
|
||
#[cfg(feature = "git")]
|
||
fn install_git_private_https_pat_at_ref() {
|
||
let context = TestContext::new("3.8");
|
||
let token = decode_token(READ_ONLY_GITHUB_TOKEN);
|
||
|
||
let mut filters: Vec<_> = [(token.as_str(), "***")]
|
||
.into_iter()
|
||
.chain(context.filters())
|
||
.collect();
|
||
|
||
filters.push((r"git\+https://", ""));
|
||
|
||
// A user is _required_ on Windows
|
||
let user = if cfg!(windows) {
|
||
filters.push((r"git:", ""));
|
||
"git:"
|
||
} else {
|
||
""
|
||
};
|
||
|
||
let package = format!("uv-private-pypackage @ git+https://{user}{token}@github.com/astral-test/uv-private-pypackage@6c09ce9ae81f50670a60abd7d95f30dd416d00ac");
|
||
uv_snapshot!(filters, context.install()
|
||
.arg(package), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 1 package in [TIME]
|
||
Prepared 1 package in [TIME]
|
||
Installed 1 package in [TIME]
|
||
+ uv-private-pypackage==0.1.0 (from ***@github.com/astral-test/uv-private-pypackage@6c09ce9ae81f50670a60abd7d95f30dd416d00ac)
|
||
"###);
|
||
|
||
context.assert_installed("uv_private_pypackage", "0.1.0");
|
||
}
|
||
|
||
/// Install a package from a private GitHub repository using a PAT and username
|
||
/// An arbitrary username is supported when using a PAT.
|
||
///
|
||
/// TODO(charlie): This test modifies the user's keyring.
|
||
/// See: <https://github.com/astral-sh/uv/issues/1980>.
|
||
#[test]
|
||
#[cfg(feature = "git")]
|
||
#[ignore]
|
||
fn install_git_private_https_pat_and_username() {
|
||
let context = TestContext::new("3.8");
|
||
let token = decode_token(READ_ONLY_GITHUB_TOKEN);
|
||
let user = "astral-test-bot";
|
||
|
||
let filters: Vec<_> = [(token.as_str(), "***")]
|
||
.into_iter()
|
||
.chain(context.filters())
|
||
.collect();
|
||
|
||
uv_snapshot!(filters, context.install().arg(format!("uv-private-pypackage @ git+https://{user}:{token}@github.com/astral-test/uv-private-pypackage"))
|
||
, @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 1 package in [TIME]
|
||
Prepared 1 package in [TIME]
|
||
Installed 1 package in [TIME]
|
||
+ uv-private-pypackage==0.1.0 (from git+https://astral-test-bot:***@github.com/astral-test/uv-private-pypackage@6c09ce9ae81f50670a60abd7d95f30dd416d00ac)
|
||
"###);
|
||
|
||
context.assert_installed("uv_private_pypackage", "0.1.0");
|
||
}
|
||
|
||
/// Install a package from a private GitHub repository using a PAT
|
||
#[test]
|
||
#[cfg(all(not(windows), feature = "git"))]
|
||
fn install_git_private_https_pat_not_authorized() {
|
||
let context = TestContext::new("3.8");
|
||
|
||
// A revoked token
|
||
let token = "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";
|
||
|
||
let mut filters = context.filters();
|
||
filters.insert(0, (token, "***"));
|
||
|
||
// We provide a username otherwise (since the token is invalid), the git cli will prompt for a password
|
||
// and hang the test
|
||
uv_snapshot!(filters, context.install()
|
||
.arg(format!("uv-private-pypackage @ git+https://git:{token}@github.com/astral-test/uv-private-pypackage"))
|
||
, @r###"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: Failed to download and build: `uv-private-pypackage @ git+https://git:***@github.com/astral-test/uv-private-pypackage`
|
||
Caused by: Git operation failed
|
||
Caused by: failed to clone into: [CACHE_DIR]/git-v0/db/8401f5508e3e612d
|
||
Caused by: process didn't exit successfully: `git fetch --force --update-head-ok 'https://git:***@github.com/astral-test/uv-private-pypackage' '+HEAD:refs/remotes/origin/HEAD'` (exit status: 128)
|
||
--- stderr
|
||
remote: Support for password authentication was removed on August 13, 2021.
|
||
remote: Please see https://docs.github.com/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
|
||
fatal: Authentication failed for 'https://github.com/astral-test/uv-private-pypackage/'
|
||
|
||
"###);
|
||
}
|
||
|
||
/// Install a package from a private GitHub repository using a PAT
|
||
/// Does not use `git`, instead installs a distribution artifact.
|
||
/// Include a public GitHub repository too, to ensure that the authentication is not erroneously copied over.
|
||
#[test]
|
||
#[cfg(not(windows))]
|
||
fn install_github_artifact_private_https_pat_mixed_with_public() {
|
||
let context = TestContext::new("3.8");
|
||
let token = decode_token(READ_ONLY_GITHUB_TOKEN);
|
||
|
||
let filters: Vec<_> = [(token.as_str(), "***")]
|
||
.into_iter()
|
||
.chain(context.filters())
|
||
.collect();
|
||
|
||
let private_package = format!(
|
||
"uv-private-pypackage @ https://{token}@raw.githubusercontent.com/astral-test/uv-private-pypackage/main/dist/uv_private_pypackage-0.1.0-py3-none-any.whl"
|
||
);
|
||
let public_package = "uv-public-pypackage @ https://raw.githubusercontent.com/astral-test/uv-public-pypackage/main/dist/uv_public_pypackage-0.1.0-py3-none-any.whl";
|
||
|
||
uv_snapshot!(filters, context.install().arg(private_package).arg(public_package),
|
||
@r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 2 packages in [TIME]
|
||
Prepared 2 packages in [TIME]
|
||
Installed 2 packages in [TIME]
|
||
+ uv-private-pypackage==0.1.0 (from https://***@raw.githubusercontent.com/astral-test/uv-private-pypackage/main/dist/uv_private_pypackage-0.1.0-py3-none-any.whl)
|
||
+ uv-public-pypackage==0.1.0 (from https://raw.githubusercontent.com/astral-test/uv-public-pypackage/main/dist/uv_public_pypackage-0.1.0-py3-none-any.whl)
|
||
"###);
|
||
|
||
context.assert_installed("uv_private_pypackage", "0.1.0");
|
||
}
|
||
|
||
/// Install packages from multiple private GitHub repositories with separate PATS
|
||
/// Does not use `git`, instead installs a distribution artifact.
|
||
#[test]
|
||
#[cfg(not(windows))]
|
||
fn install_github_artifact_private_https_multiple_pat() {
|
||
let context = TestContext::new("3.8");
|
||
let token_1 = decode_token(READ_ONLY_GITHUB_TOKEN);
|
||
let token_2 = decode_token(READ_ONLY_GITHUB_TOKEN_2);
|
||
|
||
let filters: Vec<_> = [(token_1.as_str(), "***_1"), (token_2.as_str(), "***_2")]
|
||
.into_iter()
|
||
.chain(context.filters())
|
||
.collect();
|
||
|
||
let package_1 = format!(
|
||
"uv-private-pypackage @ https://astral-test-bot:{token_1}@raw.githubusercontent.com/astral-test/uv-private-pypackage/main/dist/uv_private_pypackage-0.1.0-py3-none-any.whl"
|
||
);
|
||
let package_2 = format!(
|
||
"uv-private-pypackage-2 @ https://astral-test-bot:{token_2}@raw.githubusercontent.com/astral-test/uv-private-pypackage-2/main/dist/uv_private_pypackage_2-0.1.0-py3-none-any.whl"
|
||
);
|
||
|
||
uv_snapshot!(filters, context.install().arg(package_1).arg(package_2)
|
||
, @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 2 packages in [TIME]
|
||
Prepared 2 packages in [TIME]
|
||
Installed 2 packages in [TIME]
|
||
+ uv-private-pypackage==0.1.0 (from https://astral-test-bot:***_1@raw.githubusercontent.com/astral-test/uv-private-pypackage/main/dist/uv_private_pypackage-0.1.0-py3-none-any.whl)
|
||
+ uv-private-pypackage-2==0.1.0 (from https://astral-test-bot:***_2@raw.githubusercontent.com/astral-test/uv-private-pypackage-2/main/dist/uv_private_pypackage_2-0.1.0-py3-none-any.whl)
|
||
"###);
|
||
|
||
context.assert_installed("uv_private_pypackage", "0.1.0");
|
||
}
|
||
|
||
/// Install a package without using pre-built wheels.
|
||
#[test]
|
||
fn reinstall_no_binary() {
|
||
let context = TestContext::new("3.12");
|
||
|
||
// The first installation should use a pre-built wheel
|
||
let mut command = context.install();
|
||
command.arg("anyio").arg("--strict");
|
||
uv_snapshot!(
|
||
command,
|
||
@r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 3 packages in [TIME]
|
||
Prepared 3 packages in [TIME]
|
||
Installed 3 packages in [TIME]
|
||
+ anyio==4.3.0
|
||
+ idna==3.6
|
||
+ sniffio==1.3.1
|
||
"###
|
||
);
|
||
|
||
context.assert_command("import anyio").success();
|
||
|
||
// Running installation again with `--no-binary` should be a no-op
|
||
// The first installation should use a pre-built wheel
|
||
let mut command = context.install();
|
||
command
|
||
.arg("anyio")
|
||
.arg("--no-binary")
|
||
.arg(":all:")
|
||
.arg("--strict");
|
||
uv_snapshot!(command, @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Audited 1 package in [TIME]
|
||
"###
|
||
);
|
||
|
||
context.assert_command("import anyio").success();
|
||
|
||
// With `--reinstall`, `--no-binary` should have an affect
|
||
let filters = if cfg!(windows) {
|
||
// Remove the colorama count on windows
|
||
context
|
||
.filters()
|
||
.into_iter()
|
||
.chain([("Resolved 8 packages", "Resolved 7 packages")])
|
||
.collect()
|
||
} else {
|
||
context.filters()
|
||
};
|
||
|
||
let mut command = context.install();
|
||
command
|
||
.arg("anyio")
|
||
.arg("--no-binary")
|
||
.arg(":all:")
|
||
.arg("--reinstall-package")
|
||
.arg("anyio")
|
||
.arg("--strict");
|
||
uv_snapshot!(filters, command, @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 3 packages in [TIME]
|
||
Uninstalled 1 package in [TIME]
|
||
Installed 1 package in [TIME]
|
||
- anyio==4.3.0
|
||
+ anyio==4.3.0
|
||
"###
|
||
);
|
||
|
||
context.assert_command("import anyio").success();
|
||
}
|
||
|
||
/// Overlapping usage of `--no-binary` and `--only-binary`
|
||
#[test]
|
||
fn install_no_binary_overrides_only_binary_all() {
|
||
let context = TestContext::new("3.12");
|
||
|
||
// The specific `--no-binary` should override the less specific `--only-binary`
|
||
let mut command = context.install();
|
||
command
|
||
.arg("anyio")
|
||
.arg("--only-binary")
|
||
.arg(":all:")
|
||
.arg("--no-binary")
|
||
.arg("idna")
|
||
.arg("--strict");
|
||
uv_snapshot!(
|
||
command,
|
||
@r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 3 packages in [TIME]
|
||
Prepared 3 packages in [TIME]
|
||
Installed 3 packages in [TIME]
|
||
+ anyio==4.3.0
|
||
+ idna==3.6
|
||
+ sniffio==1.3.1
|
||
"###
|
||
);
|
||
|
||
context.assert_command("import anyio").success();
|
||
}
|
||
|
||
/// Overlapping usage of `--no-binary` and `--only-binary`
|
||
#[test]
|
||
fn install_only_binary_overrides_no_binary_all() {
|
||
let context = TestContext::new("3.12");
|
||
|
||
// The specific `--only-binary` should override the less specific `--no-binary`
|
||
let mut command = context.install();
|
||
command
|
||
.arg("anyio")
|
||
.arg("--no-binary")
|
||
.arg(":all:")
|
||
.arg("--only-binary")
|
||
.arg("idna")
|
||
.arg("--strict");
|
||
uv_snapshot!(
|
||
command,
|
||
@r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 3 packages in [TIME]
|
||
Prepared 3 packages in [TIME]
|
||
Installed 3 packages in [TIME]
|
||
+ anyio==4.3.0
|
||
+ idna==3.6
|
||
+ sniffio==1.3.1
|
||
"###
|
||
);
|
||
|
||
context.assert_command("import anyio").success();
|
||
}
|
||
|
||
/// Overlapping usage of `--no-binary` and `--only-binary`
|
||
// TODO(zanieb): We should have a better error message here
|
||
#[test]
|
||
fn install_only_binary_all_and_no_binary_all() {
|
||
let context = TestContext::new("3.12");
|
||
|
||
// With both as `:all:` we can't install
|
||
let mut command = context.install();
|
||
command
|
||
.arg("anyio")
|
||
.arg("--no-binary")
|
||
.arg(":all:")
|
||
.arg("--only-binary")
|
||
.arg(":all:")
|
||
.arg("--strict");
|
||
uv_snapshot!(
|
||
command,
|
||
@r###"
|
||
success: false
|
||
exit_code: 1
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
× No solution found when resolving dependencies:
|
||
╰─▶ Because only the following versions of anyio are available:
|
||
anyio>=1.0.0,<=1.4.0
|
||
anyio>=2.0.0,<=2.2.0
|
||
anyio>=3.0.0,<=3.6.2
|
||
anyio>=3.7.0,<=3.7.1
|
||
anyio>=4.0.0
|
||
and anyio==1.0.0 has no usable wheels and building from source is disabled, we can conclude that any of:
|
||
anyio<1.1.0
|
||
anyio>1.4.0,<2.0.0
|
||
anyio>2.2.0,<3.0.0
|
||
anyio>3.6.2,<3.7.0
|
||
anyio>3.7.1,<4.0.0
|
||
cannot be used.
|
||
And because anyio==1.1.0 has no usable wheels and building from source is disabled and anyio==1.2.0 has no usable wheels and building from source is disabled, we can conclude that any of:
|
||
anyio<1.2.1
|
||
anyio>1.4.0,<2.0.0
|
||
anyio>2.2.0,<3.0.0
|
||
anyio>3.6.2,<3.7.0
|
||
anyio>3.7.1,<4.0.0
|
||
cannot be used.
|
||
And because anyio==1.2.1 has no usable wheels and building from source is disabled and anyio==1.2.2 has no usable wheels and building from source is disabled, we can conclude that any of:
|
||
anyio<1.2.3
|
||
anyio>1.4.0,<2.0.0
|
||
anyio>2.2.0,<3.0.0
|
||
anyio>3.6.2,<3.7.0
|
||
anyio>3.7.1,<4.0.0
|
||
cannot be used.
|
||
And because anyio==1.2.3 has no usable wheels and building from source is disabled and anyio==1.3.0 has no usable wheels and building from source is disabled, we can conclude that any of:
|
||
anyio<1.3.1
|
||
anyio>1.4.0,<2.0.0
|
||
anyio>2.2.0,<3.0.0
|
||
anyio>3.6.2,<3.7.0
|
||
anyio>3.7.1,<4.0.0
|
||
cannot be used.
|
||
And because anyio==1.3.1 has no usable wheels and building from source is disabled and anyio==1.4.0 has no usable wheels and building from source is disabled, we can conclude that any of:
|
||
anyio<2.0.0
|
||
anyio>2.2.0,<3.0.0
|
||
anyio>3.6.2,<3.7.0
|
||
anyio>3.7.1,<4.0.0
|
||
cannot be used.
|
||
And because anyio==2.0.0 has no usable wheels and building from source is disabled and anyio==2.0.1 has no usable wheels and building from source is disabled, we can conclude that any of:
|
||
anyio<2.0.2
|
||
anyio>2.2.0,<3.0.0
|
||
anyio>3.6.2,<3.7.0
|
||
anyio>3.7.1,<4.0.0
|
||
cannot be used.
|
||
And because anyio==2.0.2 has no usable wheels and building from source is disabled and anyio==2.1.0 has no usable wheels and building from source is disabled, we can conclude that any of:
|
||
anyio<2.2.0
|
||
anyio>2.2.0,<3.0.0
|
||
anyio>3.6.2,<3.7.0
|
||
anyio>3.7.1,<4.0.0
|
||
cannot be used.
|
||
And because anyio==2.2.0 has no usable wheels and building from source is disabled and anyio==3.0.0 has no usable wheels and building from source is disabled, we can conclude that any of:
|
||
anyio<3.0.1
|
||
anyio>3.6.2,<3.7.0
|
||
anyio>3.7.1,<4.0.0
|
||
cannot be used.
|
||
And because anyio==3.0.1 has no usable wheels and building from source is disabled and anyio==3.1.0 has no usable wheels and building from source is disabled, we can conclude that any of:
|
||
anyio<3.2.0
|
||
anyio>3.6.2,<3.7.0
|
||
anyio>3.7.1,<4.0.0
|
||
cannot be used.
|
||
And because anyio==3.2.0 has no usable wheels and building from source is disabled and anyio==3.2.1 has no usable wheels and building from source is disabled, we can conclude that any of:
|
||
anyio<3.3.0
|
||
anyio>3.6.2,<3.7.0
|
||
anyio>3.7.1,<4.0.0
|
||
cannot be used.
|
||
And because anyio==3.3.0 has no usable wheels and building from source is disabled and anyio==3.3.1 has no usable wheels and building from source is disabled, we can conclude that any of:
|
||
anyio<3.3.2
|
||
anyio>3.6.2,<3.7.0
|
||
anyio>3.7.1,<4.0.0
|
||
cannot be used.
|
||
And because anyio==3.3.2 has no usable wheels and building from source is disabled and anyio==3.3.3 has no usable wheels and building from source is disabled, we can conclude that any of:
|
||
anyio<3.3.4
|
||
anyio>3.6.2,<3.7.0
|
||
anyio>3.7.1,<4.0.0
|
||
cannot be used.
|
||
And because anyio==3.3.4 has no usable wheels and building from source is disabled and anyio==3.4.0 has no usable wheels and building from source is disabled, we can conclude that any of:
|
||
anyio<3.5.0
|
||
anyio>3.6.2,<3.7.0
|
||
anyio>3.7.1,<4.0.0
|
||
cannot be used.
|
||
And because anyio==3.5.0 has no usable wheels and building from source is disabled and anyio==3.6.0 has no usable wheels and building from source is disabled, we can conclude that any of:
|
||
anyio<3.6.1
|
||
anyio>3.6.2,<3.7.0
|
||
anyio>3.7.1,<4.0.0
|
||
cannot be used.
|
||
And because anyio==3.6.1 has no usable wheels and building from source is disabled and anyio==3.6.2 has no usable wheels and building from source is disabled, we can conclude that any of:
|
||
anyio<3.7.0
|
||
anyio>3.7.1,<4.0.0
|
||
cannot be used.
|
||
And because anyio==3.7.0 has no usable wheels and building from source is disabled and anyio==3.7.1 has no usable wheels and building from source is disabled, we can conclude that anyio<4.0.0 cannot be used.
|
||
And because anyio==4.0.0 has no usable wheels and building from source is disabled and anyio==4.1.0 has no usable wheels and building from source is disabled, we can conclude that anyio<4.2.0 cannot be used.
|
||
And because anyio==4.2.0 has no usable wheels and building from source is disabled and anyio==4.3.0 has no usable wheels and building from source is disabled, we can conclude that anyio<4.4.0 cannot be used.
|
||
And because anyio==4.4.0 has no usable wheels and building from source is disabled and you require anyio, we can conclude that the requirements are unsatisfiable.
|
||
|
||
hint: Pre-releases are available for anyio in the requested range (e.g., 4.0.0rc1), but pre-releases weren't enabled (try: `--prerelease=allow`)
|
||
"###
|
||
);
|
||
|
||
context.assert_command("import anyio").failure();
|
||
}
|
||
|
||
/// Respect `--only-binary` flags in `requirements.txt`
|
||
#[test]
|
||
fn only_binary_requirements_txt() {
|
||
let context = TestContext::new("3.12");
|
||
|
||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||
requirements_txt
|
||
.write_str(indoc! {r"
|
||
django_allauth==0.51.0
|
||
--only-binary django_allauth
|
||
"
|
||
})
|
||
.unwrap();
|
||
|
||
uv_snapshot!(context.install()
|
||
.arg("-r")
|
||
.arg("requirements.txt")
|
||
.arg("--strict"), @r###"
|
||
success: false
|
||
exit_code: 1
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
× No solution found when resolving dependencies:
|
||
╰─▶ Because django-allauth==0.51.0 has no usable wheels and building from source is disabled and you require django-allauth==0.51.0, we can conclude that the requirements are unsatisfiable.
|
||
"###
|
||
);
|
||
}
|
||
|
||
/// `--only-binary` does not apply to editable requirements
|
||
#[test]
|
||
fn only_binary_editable() {
|
||
let context = TestContext::new("3.12");
|
||
|
||
// Install the editable package.
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg("--only-binary")
|
||
.arg(":all:")
|
||
.arg("-e")
|
||
.arg(context.workspace_root.join("scripts/packages/anyio_local")), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 1 package in [TIME]
|
||
Prepared 1 package in [TIME]
|
||
Installed 1 package in [TIME]
|
||
+ anyio==4.3.0+foo (from file://[WORKSPACE]/scripts/packages/anyio_local)
|
||
"###
|
||
);
|
||
}
|
||
|
||
/// `--only-binary` does not apply to editable requirements that depend on each other
|
||
#[test]
|
||
fn only_binary_dependent_editables() {
|
||
let context = TestContext::new("3.12");
|
||
let root_path = context
|
||
.workspace_root
|
||
.join("scripts/packages/dependent_locals");
|
||
|
||
// Install the editable package.
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg("--only-binary")
|
||
.arg(":all:")
|
||
.arg("-e")
|
||
.arg(root_path.join("first_local"))
|
||
.arg("-e")
|
||
.arg(root_path.join("second_local")), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 2 packages in [TIME]
|
||
Prepared 2 packages in [TIME]
|
||
Installed 2 packages in [TIME]
|
||
+ first-local==0.1.0 (from file://[WORKSPACE]/scripts/packages/dependent_locals/first_local)
|
||
+ second-local==0.1.0 (from file://[WORKSPACE]/scripts/packages/dependent_locals/second_local)
|
||
"###
|
||
);
|
||
}
|
||
|
||
/// `--only-binary` does not apply to editable requirements, with a `setup.py` config
|
||
#[test]
|
||
fn only_binary_editable_setup_py() {
|
||
let context = TestContext::new("3.12");
|
||
|
||
// Install the editable package.
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg("--only-binary")
|
||
.arg(":all:")
|
||
.arg("-e")
|
||
.arg(context.workspace_root.join("scripts/packages/setup_py_editable")), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 8 packages in [TIME]
|
||
Prepared 8 packages in [TIME]
|
||
Installed 8 packages in [TIME]
|
||
+ anyio==4.3.0
|
||
+ certifi==2024.2.2
|
||
+ h11==0.14.0
|
||
+ httpcore==1.0.4
|
||
+ httpx==0.27.0
|
||
+ idna==3.6
|
||
+ setup-py-editable==0.0.1 (from file://[WORKSPACE]/scripts/packages/setup_py_editable)
|
||
+ sniffio==1.3.1
|
||
"###
|
||
);
|
||
}
|
||
|
||
/// Install a package into a virtual environment, and ensuring that the executable permissions
|
||
/// are retained.
|
||
///
|
||
/// This test uses the default link semantics. (On macOS, this is `clone`.)
|
||
#[test]
|
||
fn install_executable() {
|
||
let context = TestContext::new("3.12");
|
||
|
||
uv_snapshot!(context.install()
|
||
.arg("pylint==3.0.0"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 7 packages in [TIME]
|
||
Prepared 7 packages in [TIME]
|
||
Installed 7 packages in [TIME]
|
||
+ astroid==3.0.3
|
||
+ dill==0.3.8
|
||
+ isort==5.13.2
|
||
+ mccabe==0.7.0
|
||
+ platformdirs==4.2.0
|
||
+ pylint==3.0.0
|
||
+ tomlkit==0.12.4
|
||
"###
|
||
);
|
||
|
||
// Verify that `pylint` is executable.
|
||
let executable = context
|
||
.venv
|
||
.join(if cfg!(windows) { "Scripts" } else { "bin" })
|
||
.join(format!("pylint{}", std::env::consts::EXE_SUFFIX));
|
||
Command::new(executable).arg("--version").assert().success();
|
||
}
|
||
|
||
/// Install a package into a virtual environment using copy semantics, and ensure that the
|
||
/// executable permissions are retained.
|
||
#[test]
|
||
fn install_executable_copy() {
|
||
let context = TestContext::new("3.12");
|
||
|
||
uv_snapshot!(context.install()
|
||
.arg("pylint==3.0.0")
|
||
.arg("--link-mode")
|
||
.arg("copy"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 7 packages in [TIME]
|
||
Prepared 7 packages in [TIME]
|
||
Installed 7 packages in [TIME]
|
||
+ astroid==3.0.3
|
||
+ dill==0.3.8
|
||
+ isort==5.13.2
|
||
+ mccabe==0.7.0
|
||
+ platformdirs==4.2.0
|
||
+ pylint==3.0.0
|
||
+ tomlkit==0.12.4
|
||
"###
|
||
);
|
||
|
||
// Verify that `pylint` is executable.
|
||
let executable = context
|
||
.venv
|
||
.join(if cfg!(windows) { "Scripts" } else { "bin" })
|
||
.join(format!("pylint{}", std::env::consts::EXE_SUFFIX));
|
||
Command::new(executable).arg("--version").assert().success();
|
||
}
|
||
|
||
/// Install a package into a virtual environment using hardlink semantics, and ensure that the
|
||
/// executable permissions are retained.
|
||
#[test]
|
||
fn install_executable_hardlink() {
|
||
let context = TestContext::new("3.12");
|
||
|
||
uv_snapshot!(context.install()
|
||
.arg("pylint==3.0.0")
|
||
.arg("--link-mode")
|
||
.arg("hardlink"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 7 packages in [TIME]
|
||
Prepared 7 packages in [TIME]
|
||
Installed 7 packages in [TIME]
|
||
+ astroid==3.0.3
|
||
+ dill==0.3.8
|
||
+ isort==5.13.2
|
||
+ mccabe==0.7.0
|
||
+ platformdirs==4.2.0
|
||
+ pylint==3.0.0
|
||
+ tomlkit==0.12.4
|
||
"###
|
||
);
|
||
|
||
// Verify that `pylint` is executable.
|
||
let executable = context
|
||
.venv
|
||
.join(if cfg!(windows) { "Scripts" } else { "bin" })
|
||
.join(format!("pylint{}", std::env::consts::EXE_SUFFIX));
|
||
Command::new(executable).arg("--version").assert().success();
|
||
}
|
||
|
||
/// Install a package from the command line into a virtual environment, ignoring its dependencies.
|
||
#[test]
|
||
fn no_deps() {
|
||
let context = TestContext::new("3.12");
|
||
|
||
// Install Flask.
|
||
uv_snapshot!(context.install()
|
||
.arg("Flask")
|
||
.arg("--no-deps")
|
||
.arg("--strict"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 1 package in [TIME]
|
||
Prepared 1 package in [TIME]
|
||
Installed 1 package in [TIME]
|
||
+ flask==3.0.2
|
||
warning: The package `flask` requires `werkzeug>=3.0.0`, but it's not installed.
|
||
warning: The package `flask` requires `jinja2>=3.1.2`, but it's not installed.
|
||
warning: The package `flask` requires `itsdangerous>=2.1.2`, but it's not installed.
|
||
warning: The package `flask` requires `click>=8.1.3`, but it's not installed.
|
||
warning: The package `flask` requires `blinker>=1.6.2`, but it's not installed.
|
||
"###
|
||
);
|
||
|
||
context.assert_command("import flask").failure();
|
||
}
|
||
|
||
/// Install an editable package from the command line into a virtual environment, ignoring its
|
||
/// dependencies.
|
||
#[test]
|
||
fn no_deps_editable() {
|
||
let context = TestContext::new("3.12");
|
||
|
||
// Install the editable version of Black. This should remove the registry-based version.
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg("--no-deps")
|
||
.arg("-e")
|
||
.arg(context.workspace_root.join("scripts/packages/black_editable[dev]")), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 1 package in [TIME]
|
||
Prepared 1 package in [TIME]
|
||
Installed 1 package in [TIME]
|
||
+ black==0.1.0 (from file://[WORKSPACE]/scripts/packages/black_editable)
|
||
"###
|
||
);
|
||
|
||
context.assert_command("import black").success();
|
||
context.assert_command("import aiohttp").failure();
|
||
}
|
||
|
||
/// Upgrade a package.
|
||
#[test]
|
||
fn install_upgrade() {
|
||
let context = TestContext::new("3.12");
|
||
|
||
// Install an old version of anyio and httpcore.
|
||
uv_snapshot!(context.install()
|
||
.arg("anyio==3.6.2")
|
||
.arg("httpcore==0.16.3")
|
||
.arg("--strict"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 6 packages in [TIME]
|
||
Prepared 6 packages in [TIME]
|
||
Installed 6 packages in [TIME]
|
||
+ anyio==3.6.2
|
||
+ certifi==2024.2.2
|
||
+ h11==0.14.0
|
||
+ httpcore==0.16.3
|
||
+ idna==3.6
|
||
+ sniffio==1.3.1
|
||
"###
|
||
);
|
||
|
||
context.assert_command("import anyio").success();
|
||
|
||
// Upgrade anyio.
|
||
uv_snapshot!(context.install()
|
||
.arg("anyio")
|
||
.arg("--upgrade-package")
|
||
.arg("anyio"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 3 packages in [TIME]
|
||
Prepared 1 package in [TIME]
|
||
Uninstalled 1 package in [TIME]
|
||
Installed 1 package in [TIME]
|
||
- anyio==3.6.2
|
||
+ anyio==4.3.0
|
||
"###
|
||
);
|
||
|
||
// Upgrade anyio again, should not reinstall.
|
||
uv_snapshot!(context.install()
|
||
.arg("anyio")
|
||
.arg("--upgrade-package")
|
||
.arg("anyio"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 3 packages in [TIME]
|
||
Audited 3 packages in [TIME]
|
||
"###
|
||
);
|
||
|
||
// Install httpcore, request anyio upgrade should not reinstall
|
||
uv_snapshot!(context.install()
|
||
.arg("httpcore")
|
||
.arg("--upgrade-package")
|
||
.arg("anyio"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 6 packages in [TIME]
|
||
Audited 6 packages in [TIME]
|
||
"###
|
||
);
|
||
|
||
// Upgrade httpcore with global flag
|
||
uv_snapshot!(context.install()
|
||
.arg("httpcore")
|
||
.arg("--upgrade"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 3 packages in [TIME]
|
||
Prepared 1 package in [TIME]
|
||
Uninstalled 1 package in [TIME]
|
||
Installed 1 package in [TIME]
|
||
- httpcore==0.16.3
|
||
+ httpcore==1.0.4
|
||
"###
|
||
);
|
||
}
|
||
|
||
/// Install a package from a `requirements.txt` file, with a `constraints.txt` file.
|
||
#[test]
|
||
fn install_constraints_txt() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||
requirements_txt.write_str("anyio==3.7.0")?;
|
||
|
||
let constraints_txt = context.temp_dir.child("constraints.txt");
|
||
constraints_txt.write_str("idna<3.4")?;
|
||
|
||
uv_snapshot!(context.install()
|
||
.arg("-r")
|
||
.arg("requirements.txt")
|
||
.arg("--constraint")
|
||
.arg("constraints.txt"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 3 packages in [TIME]
|
||
Prepared 3 packages in [TIME]
|
||
Installed 3 packages in [TIME]
|
||
+ anyio==3.7.0
|
||
+ idna==3.3
|
||
+ sniffio==1.3.1
|
||
"###
|
||
);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Install a package from a `requirements.txt` file, with an inline constraint.
|
||
#[test]
|
||
fn install_constraints_inline() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
let requirementstxt = context.temp_dir.child("requirements.txt");
|
||
requirementstxt.write_str("anyio==3.7.0\n-c constraints.txt")?;
|
||
|
||
let constraints_txt = context.temp_dir.child("constraints.txt");
|
||
constraints_txt.write_str("idna<3.4")?;
|
||
|
||
uv_snapshot!(context.install()
|
||
.arg("-r")
|
||
.arg("requirements.txt"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 3 packages in [TIME]
|
||
Prepared 3 packages in [TIME]
|
||
Installed 3 packages in [TIME]
|
||
+ anyio==3.7.0
|
||
+ idna==3.3
|
||
+ sniffio==1.3.1
|
||
"###
|
||
);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Install a package from a `constraints.txt` file on a remote http server.
|
||
#[test]
|
||
fn install_constraints_remote() {
|
||
let context = TestContext::new("3.12");
|
||
|
||
uv_snapshot!(context.install()
|
||
.arg("-c")
|
||
.arg("https://raw.githubusercontent.com/apache/airflow/constraints-2-6/constraints-3.11.txt")
|
||
.arg("typing_extensions>=4.0"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 1 package in [TIME]
|
||
Prepared 1 package in [TIME]
|
||
Installed 1 package in [TIME]
|
||
+ typing-extensions==4.7.1
|
||
"###
|
||
); // would yield typing-extensions==4.8.2 without constraint file
|
||
}
|
||
|
||
/// Install a package from a `requirements.txt` file, with an inline constraint, which points
|
||
/// to a remote http server.
|
||
#[test]
|
||
fn install_constraints_inline_remote() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
let requirementstxt = context.temp_dir.child("requirements.txt");
|
||
requirementstxt.write_str("typing-extensions>=4.0\n-c https://raw.githubusercontent.com/apache/airflow/constraints-2-6/constraints-3.11.txt")?;
|
||
|
||
uv_snapshot!(context.install()
|
||
.arg("-r")
|
||
.arg("requirements.txt"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 1 package in [TIME]
|
||
Prepared 1 package in [TIME]
|
||
Installed 1 package in [TIME]
|
||
+ typing-extensions==4.7.1
|
||
"### // would yield typing-extensions==4.8.2 without constraint file
|
||
);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Constrain a package that's included via an extra.
|
||
#[test]
|
||
fn install_constraints_extra() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||
requirements_txt.write_str("flask[dotenv]")?;
|
||
|
||
let constraints_txt = context.temp_dir.child("constraints.txt");
|
||
constraints_txt.write_str("python-dotenv==1.0.0")?;
|
||
|
||
uv_snapshot!(context.install()
|
||
.arg("-r")
|
||
.arg("requirements.txt")
|
||
.arg("-c")
|
||
.arg("constraints.txt"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 8 packages in [TIME]
|
||
Prepared 8 packages in [TIME]
|
||
Installed 8 packages in [TIME]
|
||
+ blinker==1.7.0
|
||
+ click==8.1.7
|
||
+ flask==3.0.2
|
||
+ itsdangerous==2.1.2
|
||
+ jinja2==3.1.3
|
||
+ markupsafe==2.1.5
|
||
+ python-dotenv==1.0.0
|
||
+ werkzeug==3.0.1
|
||
"###
|
||
);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn install_constraints_respects_offline_mode() {
|
||
let context = TestContext::new("3.12");
|
||
|
||
uv_snapshot!(context.install()
|
||
.arg("--offline")
|
||
.arg("-r")
|
||
.arg("http://example.com/requirements.txt"), @r###"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: Network connectivity is disabled, but a remote requirements file was requested: http://example.com/requirements.txt
|
||
"###
|
||
);
|
||
}
|
||
|
||
/// Tests that we can install `polars==0.14.0`, which has this odd dependency
|
||
/// requirement in its wheel metadata: `pyarrow>=4.0.*; extra == 'pyarrow'`.
|
||
///
|
||
/// The `>=4.0.*` is invalid, but is something we "fix" because it is out
|
||
/// of the control of the end user. However, our fix for this case ends up
|
||
/// stripping the quotes around `pyarrow` and thus produces an irrevocably
|
||
/// invalid dependency requirement.
|
||
///
|
||
/// See: <https://github.com/astral-sh/uv/issues/1477>
|
||
#[test]
|
||
fn install_pinned_polars_invalid_metadata() {
|
||
let context = TestContext::new("3.12");
|
||
|
||
// Install Flask.
|
||
uv_snapshot!(context.install()
|
||
.arg("polars==0.14.0"),
|
||
@r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 1 package in [TIME]
|
||
Prepared 1 package in [TIME]
|
||
Installed 1 package in [TIME]
|
||
+ polars==0.14.0
|
||
"###
|
||
);
|
||
|
||
context.assert_command("import polars").success();
|
||
}
|
||
|
||
/// Install a source distribution with `--resolution=lowest-direct`, to ensure that the build
|
||
/// requirements aren't resolved at their lowest compatible version.
|
||
#[test]
|
||
fn install_sdist_resolution_lowest() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
let requirements_in = context.temp_dir.child("requirements.in");
|
||
requirements_in.write_str("anyio @ https://files.pythonhosted.org/packages/2d/b8/7333d87d5f03247215d86a86362fd3e324111788c6cdd8d2e6196a6ba833/anyio-4.2.0.tar.gz")?;
|
||
|
||
uv_snapshot!(context.install()
|
||
.arg("-r")
|
||
.arg("requirements.in")
|
||
.arg("--resolution=lowest-direct"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 3 packages in [TIME]
|
||
Prepared 3 packages in [TIME]
|
||
Installed 3 packages in [TIME]
|
||
+ anyio==4.2.0 (from https://files.pythonhosted.org/packages/2d/b8/7333d87d5f03247215d86a86362fd3e324111788c6cdd8d2e6196a6ba833/anyio-4.2.0.tar.gz)
|
||
+ idna==3.6
|
||
+ sniffio==1.3.1
|
||
"###
|
||
);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Tests that we can install a package from a zip file that has bunk
|
||
/// permissions.
|
||
///
|
||
/// See: <https://github.com/astral-sh/uv/issues/1453>
|
||
#[test]
|
||
fn direct_url_zip_file_bunk_permissions() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||
requirements_txt.write_str(
|
||
"opensafely-pipeline @ https://github.com/opensafely-core/pipeline/archive/refs/tags/v2023.11.06.145820.zip",
|
||
)?;
|
||
|
||
uv_snapshot!(context.install()
|
||
.arg("-r")
|
||
.arg("requirements.txt")
|
||
.arg("--strict"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 6 packages in [TIME]
|
||
Prepared 5 packages in [TIME]
|
||
Installed 6 packages in [TIME]
|
||
+ distro==1.9.0
|
||
+ opensafely-pipeline==2023.11.6.145820 (from https://github.com/opensafely-core/pipeline/archive/refs/tags/v2023.11.06.145820.zip)
|
||
+ pydantic==1.10.14
|
||
+ ruyaml==0.91.0
|
||
+ setuptools==69.2.0
|
||
+ typing-extensions==4.10.0
|
||
"###
|
||
);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn launcher() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
let project_root = fs_err::canonicalize(std::env::current_dir()?.join("../.."))?;
|
||
|
||
let filters = [
|
||
(r"(\d+m )?(\d+\.)?\d+(ms|s)", "[TIME]"),
|
||
(
|
||
r"simple-launcher==0\.1\.0 \(from .+\.whl\)",
|
||
"simple_launcher.whl",
|
||
),
|
||
];
|
||
|
||
uv_snapshot!(
|
||
filters,
|
||
context.install()
|
||
.arg(format!("simple_launcher@{}", project_root.join("scripts/links/simple_launcher-0.1.0-py3-none-any.whl").display()))
|
||
.arg("--strict"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 1 package in [TIME]
|
||
Prepared 1 package in [TIME]
|
||
Installed 1 package in [TIME]
|
||
+ simple_launcher.whl
|
||
"###
|
||
);
|
||
|
||
let bin_path = if cfg!(windows) { "Scripts" } else { "bin" };
|
||
|
||
uv_snapshot!(Command::new(
|
||
context.venv.join(bin_path).join("simple_launcher")
|
||
), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
Hi from the simple launcher!
|
||
|
||
----- stderr -----
|
||
"###);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn launcher_with_symlink() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
let project_root = fs_err::canonicalize(std::env::current_dir()?.join("../.."))?;
|
||
|
||
let filters = [
|
||
(r"(\d+m )?(\d+\.)?\d+(ms|s)", "[TIME]"),
|
||
(
|
||
r"simple-launcher==0\.1\.0 \(from .+\.whl\)",
|
||
"simple_launcher.whl",
|
||
),
|
||
];
|
||
|
||
uv_snapshot!(filters,
|
||
context.install()
|
||
.arg(format!("simple_launcher@{}", project_root.join("scripts/links/simple_launcher-0.1.0-py3-none-any.whl").display()))
|
||
.arg("--strict"),
|
||
@r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 1 package in [TIME]
|
||
Prepared 1 package in [TIME]
|
||
Installed 1 package in [TIME]
|
||
+ simple_launcher.whl
|
||
"###
|
||
);
|
||
|
||
#[cfg(windows)]
|
||
if let Err(error) = std::os::windows::fs::symlink_file(
|
||
context.venv.join("Scripts\\simple_launcher.exe"),
|
||
context.temp_dir.join("simple_launcher.exe"),
|
||
) {
|
||
// Os { code: 1314, kind: Uncategorized, message: "A required privilege is not held by the client." }
|
||
// where `Uncategorized` is unstable.
|
||
if error.raw_os_error() == Some(1314) {
|
||
return Ok(());
|
||
}
|
||
return Err(error.into());
|
||
}
|
||
|
||
#[cfg(unix)]
|
||
std::os::unix::fs::symlink(
|
||
context.venv.join("bin/simple_launcher"),
|
||
context.temp_dir.join("simple_launcher"),
|
||
)?;
|
||
|
||
// Only support windows or linux
|
||
#[cfg(not(any(windows, unix)))]
|
||
return Ok(());
|
||
|
||
uv_snapshot!(Command::new(
|
||
context.temp_dir.join("simple_launcher")
|
||
), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
Hi from the simple launcher!
|
||
|
||
----- stderr -----
|
||
"###);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn config_settings() {
|
||
let context = TestContext::new("3.12");
|
||
|
||
// Install the editable package.
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg("-e")
|
||
.arg(context.workspace_root.join("scripts/packages/setuptools_editable")), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 2 packages in [TIME]
|
||
Prepared 2 packages in [TIME]
|
||
Installed 2 packages in [TIME]
|
||
+ iniconfig==2.0.0
|
||
+ setuptools-editable==0.1.0 (from file://[WORKSPACE]/scripts/packages/setuptools_editable)
|
||
"###
|
||
);
|
||
|
||
// When installed without `--editable_mode=compat`, the `finder.py` file should be present.
|
||
let finder = context
|
||
.site_packages()
|
||
.join("__editable___setuptools_editable_0_1_0_finder.py");
|
||
assert!(finder.exists());
|
||
|
||
// Install the editable package with `--editable_mode=compat`.
|
||
let context = TestContext::new("3.12");
|
||
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg("-e")
|
||
.arg(context.workspace_root.join("scripts/packages/setuptools_editable"))
|
||
.arg("-C")
|
||
.arg("editable_mode=compat")
|
||
, @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 2 packages in [TIME]
|
||
Prepared 2 packages in [TIME]
|
||
Installed 2 packages in [TIME]
|
||
+ iniconfig==2.0.0
|
||
+ setuptools-editable==0.1.0 (from file://[WORKSPACE]/scripts/packages/setuptools_editable)
|
||
"###
|
||
);
|
||
|
||
// When installed without `--editable_mode=compat`, the `finder.py` file should _not_ be present.
|
||
let finder = context
|
||
.site_packages()
|
||
.join("__editable___setuptools_editable_0_1_0_finder.py");
|
||
assert!(!finder.exists());
|
||
}
|
||
|
||
/// Reinstall a duplicate package in a virtual environment.
|
||
#[test]
|
||
fn reinstall_duplicate() -> Result<()> {
|
||
use crate::common::copy_dir_all;
|
||
|
||
// Sync a version of `pip` into a virtual environment.
|
||
let context1 = TestContext::new("3.12");
|
||
let requirements_txt = context1.temp_dir.child("requirements.txt");
|
||
requirements_txt.write_str("pip==21.3.1")?;
|
||
|
||
// Run `pip sync`.
|
||
context1
|
||
.install()
|
||
.arg("-r")
|
||
.arg(requirements_txt.path())
|
||
.assert()
|
||
.success();
|
||
|
||
// Sync a different version of `pip` into a virtual environment.
|
||
let context2 = TestContext::new("3.12");
|
||
let requirements_txt = context2.temp_dir.child("requirements.txt");
|
||
requirements_txt.write_str("pip==22.1.1")?;
|
||
|
||
// Run `pip sync`.
|
||
context2
|
||
.install()
|
||
.arg("-r")
|
||
.arg(requirements_txt.path())
|
||
.assert()
|
||
.success();
|
||
|
||
// Copy the virtual environment to a new location.
|
||
copy_dir_all(
|
||
context2.site_packages().join("pip-22.1.1.dist-info"),
|
||
context1.site_packages().join("pip-22.1.1.dist-info"),
|
||
)?;
|
||
|
||
// Run `pip install`.
|
||
uv_snapshot!(context1.install()
|
||
.arg("pip")
|
||
.arg("--reinstall"),
|
||
@r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 1 package in [TIME]
|
||
Prepared 1 package in [TIME]
|
||
Uninstalled 2 packages in [TIME]
|
||
Installed 1 package in [TIME]
|
||
- pip==21.3.1
|
||
- pip==22.1.1
|
||
+ pip==24.0
|
||
"###
|
||
);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Install a package that contains a symlink within the archive.
|
||
#[test]
|
||
fn install_symlink() {
|
||
let context = TestContext::new("3.12");
|
||
|
||
uv_snapshot!(context.install()
|
||
.arg("pgpdump==1.5")
|
||
.arg("--strict"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 1 package in [TIME]
|
||
Prepared 1 package in [TIME]
|
||
Installed 1 package in [TIME]
|
||
+ pgpdump==1.5
|
||
"###
|
||
);
|
||
|
||
context.assert_command("import pgpdump").success();
|
||
|
||
uv_snapshot!(uninstall_command(&context)
|
||
.arg("pgpdump"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Uninstalled 1 package in [TIME]
|
||
- pgpdump==1.5
|
||
"###
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn invalidate_editable_on_change() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
// Create an editable package.
|
||
let editable_dir = context.temp_dir.child("editable");
|
||
editable_dir.create_dir_all()?;
|
||
let pyproject_toml = editable_dir.child("pyproject.toml");
|
||
pyproject_toml.write_str(
|
||
r#"[project]
|
||
name = "example"
|
||
version = "0.0.0"
|
||
dependencies = [
|
||
"anyio==4.0.0"
|
||
]
|
||
requires-python = ">=3.8"
|
||
"#,
|
||
)?;
|
||
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg("--editable")
|
||
.arg(editable_dir.path()), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 4 packages in [TIME]
|
||
Prepared 4 packages in [TIME]
|
||
Installed 4 packages in [TIME]
|
||
+ anyio==4.0.0
|
||
+ example==0.0.0 (from file://[TEMP_DIR]/editable)
|
||
+ idna==3.6
|
||
+ sniffio==1.3.1
|
||
"###
|
||
);
|
||
|
||
// Re-installing should be a no-op.
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg("--editable")
|
||
.arg(editable_dir.path()), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Audited 1 package in [TIME]
|
||
"###
|
||
);
|
||
|
||
// Modify the editable package.
|
||
pyproject_toml.write_str(
|
||
r#"[project]
|
||
name = "example"
|
||
version = "0.0.0"
|
||
dependencies = [
|
||
"anyio==3.7.1"
|
||
]
|
||
requires-python = ">=3.8"
|
||
"#,
|
||
)?;
|
||
|
||
// Re-installing should update the package.
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg("--editable")
|
||
.arg(editable_dir.path()), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 4 packages in [TIME]
|
||
Prepared 2 packages in [TIME]
|
||
Uninstalled 2 packages in [TIME]
|
||
Installed 2 packages in [TIME]
|
||
- anyio==4.0.0
|
||
+ anyio==3.7.1
|
||
- example==0.0.0 (from file://[TEMP_DIR]/editable)
|
||
+ example==0.0.0 (from file://[TEMP_DIR]/editable)
|
||
"###
|
||
);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn invalidate_editable_dynamic() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
// Create an editable package with dynamic metadata
|
||
let editable_dir = context.temp_dir.child("editable");
|
||
editable_dir.create_dir_all()?;
|
||
let pyproject_toml = editable_dir.child("pyproject.toml");
|
||
pyproject_toml.write_str(
|
||
r#"
|
||
[project]
|
||
name = "example"
|
||
version = "0.1.0"
|
||
dynamic = ["dependencies"]
|
||
requires-python = ">=3.11,<3.13"
|
||
|
||
[tool.setuptools.dynamic]
|
||
dependencies = {file = ["requirements.txt"]}
|
||
"#,
|
||
)?;
|
||
|
||
let requirements_txt = editable_dir.child("requirements.txt");
|
||
requirements_txt.write_str("anyio==4.0.0")?;
|
||
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg("--editable")
|
||
.arg(editable_dir.path()), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 4 packages in [TIME]
|
||
Prepared 4 packages in [TIME]
|
||
Installed 4 packages in [TIME]
|
||
+ anyio==4.0.0
|
||
+ example==0.1.0 (from file://[TEMP_DIR]/editable)
|
||
+ idna==3.6
|
||
+ sniffio==1.3.1
|
||
"###
|
||
);
|
||
|
||
// Re-installing should re-install.
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg("--editable")
|
||
.arg(editable_dir.path()), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 4 packages in [TIME]
|
||
Prepared 1 package in [TIME]
|
||
Uninstalled 1 package in [TIME]
|
||
Installed 1 package in [TIME]
|
||
- example==0.1.0 (from file://[TEMP_DIR]/editable)
|
||
+ example==0.1.0 (from file://[TEMP_DIR]/editable)
|
||
"###
|
||
);
|
||
|
||
// Modify the requirements.
|
||
requirements_txt.write_str("anyio==3.7.1")?;
|
||
|
||
// Re-installing should update the package.
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg("--editable")
|
||
.arg(editable_dir.path()), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 4 packages in [TIME]
|
||
Prepared 2 packages in [TIME]
|
||
Uninstalled 2 packages in [TIME]
|
||
Installed 2 packages in [TIME]
|
||
- anyio==4.0.0
|
||
+ anyio==3.7.1
|
||
- example==0.1.0 (from file://[TEMP_DIR]/editable)
|
||
+ example==0.1.0 (from file://[TEMP_DIR]/editable)
|
||
"###
|
||
);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn invalidate_path_on_change() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
// Create a local package.
|
||
let editable_dir = context.temp_dir.child("editable");
|
||
editable_dir.create_dir_all()?;
|
||
let pyproject_toml = editable_dir.child("pyproject.toml");
|
||
pyproject_toml.write_str(
|
||
r#"[project]
|
||
name = "example"
|
||
version = "0.0.0"
|
||
dependencies = [
|
||
"anyio==4.0.0"
|
||
]
|
||
requires-python = ">=3.8"
|
||
"#,
|
||
)?;
|
||
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg("example @ .")
|
||
.current_dir(editable_dir.path()), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 4 packages in [TIME]
|
||
Prepared 4 packages in [TIME]
|
||
Installed 4 packages in [TIME]
|
||
+ anyio==4.0.0
|
||
+ example==0.0.0 (from file://[TEMP_DIR]/editable)
|
||
+ idna==3.6
|
||
+ sniffio==1.3.1
|
||
"###
|
||
);
|
||
|
||
// Re-installing should be a no-op.
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg("example @ .")
|
||
.current_dir(editable_dir.path()), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Audited 1 package in [TIME]
|
||
"###
|
||
);
|
||
|
||
// Modify the package.
|
||
pyproject_toml.write_str(
|
||
r#"[project]
|
||
name = "example"
|
||
version = "0.0.0"
|
||
dependencies = [
|
||
"anyio==3.7.1"
|
||
]
|
||
requires-python = ">=3.8"
|
||
"#,
|
||
)?;
|
||
|
||
// Re-installing should update the package.
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg("example @ .")
|
||
.current_dir(editable_dir.path()), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 4 packages in [TIME]
|
||
Prepared 2 packages in [TIME]
|
||
Uninstalled 2 packages in [TIME]
|
||
Installed 2 packages in [TIME]
|
||
- anyio==4.0.0
|
||
+ anyio==3.7.1
|
||
- example==0.0.0 (from file://[TEMP_DIR]/editable)
|
||
+ example==0.0.0 (from file://[TEMP_DIR]/editable)
|
||
"###
|
||
);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Install from a direct path (wheel) with changed versions in the path.
|
||
#[test]
|
||
fn path_version_change() {
|
||
let context = TestContext::new("3.12");
|
||
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg(context.workspace_root.join("scripts/links/ok-1.0.0-py3-none-any.whl")), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 1 package in [TIME]
|
||
Prepared 1 package in [TIME]
|
||
Installed 1 package in [TIME]
|
||
+ ok==1.0.0 (from file://[WORKSPACE]/scripts/links/ok-1.0.0-py3-none-any.whl)
|
||
"###
|
||
);
|
||
|
||
// Installing the same path again should be a no-op
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg(context.workspace_root.join("scripts/links/ok-1.0.0-py3-none-any.whl")), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Audited 1 package in [TIME]
|
||
"###
|
||
);
|
||
|
||
// Installing a new path should succeed
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg(context.workspace_root.join("scripts/links/ok-2.0.0-py3-none-any.whl")), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 1 package in [TIME]
|
||
Audited 1 package in [TIME]
|
||
"###
|
||
);
|
||
|
||
// Installing a new path should succeed regardless of which version is "newer"
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg(context.workspace_root.join("scripts/links/ok-1.0.0-py3-none-any.whl")), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Audited 1 package in [TIME]
|
||
"###
|
||
);
|
||
}
|
||
|
||
/// Ignore a URL dependency with a non-matching marker.
|
||
#[test]
|
||
fn editable_url_with_marker() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
let editable_dir = context.temp_dir.child("editable");
|
||
editable_dir.create_dir_all()?;
|
||
let pyproject_toml = editable_dir.child("pyproject.toml");
|
||
pyproject_toml.write_str(
|
||
r#"
|
||
[project]
|
||
name = "example"
|
||
version = "0.1.0"
|
||
dependencies = [
|
||
"anyio==4.0.0; python_version >= '3.11'",
|
||
"anyio @ https://files.pythonhosted.org/packages/2d/b8/7333d87d5f03247215d86a86362fd3e324111788c6cdd8d2e6196a6ba833/anyio-4.2.0.tar.gz ; python_version < '3.11'"
|
||
]
|
||
requires-python = ">=3.11,<3.13"
|
||
"#,
|
||
)?;
|
||
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg("--editable")
|
||
.arg(editable_dir.path()), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 4 packages in [TIME]
|
||
Prepared 4 packages in [TIME]
|
||
Installed 4 packages in [TIME]
|
||
+ anyio==4.0.0
|
||
+ example==0.1.0 (from file://[TEMP_DIR]/editable)
|
||
+ idna==3.6
|
||
+ sniffio==1.3.1
|
||
"###
|
||
);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Raise an error when an editable's `Requires-Python` constraint is not met.
|
||
#[test]
|
||
fn requires_python_editable() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
// Create an editable package with a `Requires-Python` constraint that is not met.
|
||
let editable_dir = context.temp_dir.child("editable");
|
||
editable_dir.create_dir_all()?;
|
||
let pyproject_toml = editable_dir.child("pyproject.toml");
|
||
pyproject_toml.write_str(
|
||
r#"[project]
|
||
name = "example"
|
||
version = "0.0.0"
|
||
dependencies = [
|
||
"anyio==4.0.0"
|
||
]
|
||
requires-python = "<=3.8"
|
||
"#,
|
||
)?;
|
||
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg("--editable")
|
||
.arg(editable_dir.path()), @r###"
|
||
success: false
|
||
exit_code: 1
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
× No solution found when resolving dependencies:
|
||
╰─▶ Because the current Python version (3.12.[X]) does not satisfy Python<=3.8 and example==0.0.0 depends on Python<=3.8, we can conclude that example==0.0.0 cannot be used.
|
||
And because only example==0.0.0 is available and you require example, we can conclude that the requirements are unsatisfiable.
|
||
"###
|
||
);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Install with `--no-build-isolation`, to disable isolation during PEP 517 builds.
|
||
#[test]
|
||
fn no_build_isolation() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
let requirements_in = context.temp_dir.child("requirements.in");
|
||
requirements_in.write_str("anyio @ https://files.pythonhosted.org/packages/db/4d/3970183622f0330d3c23d9b8a5f52e365e50381fd484d08e3285104333d3/anyio-4.3.0.tar.gz")?;
|
||
|
||
// We expect the build to fail, because `setuptools` is not installed.
|
||
let filters = std::iter::once((r"exit code: 1", "exit status: 1"))
|
||
.chain(context.filters())
|
||
.collect::<Vec<_>>();
|
||
uv_snapshot!(filters, context.install()
|
||
.arg("-r")
|
||
.arg("requirements.in")
|
||
.arg("--no-build-isolation"), @r###"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: Failed to download and build: `anyio @ https://files.pythonhosted.org/packages/db/4d/3970183622f0330d3c23d9b8a5f52e365e50381fd484d08e3285104333d3/anyio-4.3.0.tar.gz`
|
||
Caused by: Failed to build: `anyio @ https://files.pythonhosted.org/packages/db/4d/3970183622f0330d3c23d9b8a5f52e365e50381fd484d08e3285104333d3/anyio-4.3.0.tar.gz`
|
||
Caused by: Build backend failed to determine metadata through `prepare_metadata_for_build_wheel` with exit status: 1
|
||
--- stdout:
|
||
|
||
--- stderr:
|
||
Traceback (most recent call last):
|
||
File "<string>", line 8, in <module>
|
||
ModuleNotFoundError: No module named 'setuptools'
|
||
---
|
||
"###
|
||
);
|
||
|
||
// Install `setuptools` and `wheel`.
|
||
uv_snapshot!(context.install()
|
||
.arg("setuptools")
|
||
.arg("wheel"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 2 packages in [TIME]
|
||
Prepared 2 packages in [TIME]
|
||
Installed 2 packages in [TIME]
|
||
+ setuptools==69.2.0
|
||
+ wheel==0.43.0
|
||
"###);
|
||
|
||
// We expect the build to succeed, since `setuptools` is now installed.
|
||
uv_snapshot!(context.install()
|
||
.arg("-r")
|
||
.arg("requirements.in")
|
||
.arg("--no-build-isolation"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 3 packages in [TIME]
|
||
Prepared 3 packages in [TIME]
|
||
Installed 3 packages in [TIME]
|
||
+ anyio==0.0.0 (from https://files.pythonhosted.org/packages/db/4d/3970183622f0330d3c23d9b8a5f52e365e50381fd484d08e3285104333d3/anyio-4.3.0.tar.gz)
|
||
+ idna==3.6
|
||
+ sniffio==1.3.1
|
||
"###
|
||
);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Ensure that `UV_NO_BUILD_ISOLATION` env var does the same as the `--no-build-isolation` flag
|
||
#[test]
|
||
fn respect_no_build_isolation_env_var() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
let requirements_in = context.temp_dir.child("requirements.in");
|
||
requirements_in.write_str("anyio @ https://files.pythonhosted.org/packages/db/4d/3970183622f0330d3c23d9b8a5f52e365e50381fd484d08e3285104333d3/anyio-4.3.0.tar.gz")?;
|
||
|
||
// We expect the build to fail, because `setuptools` is not installed.
|
||
let filters = std::iter::once((r"exit code: 1", "exit status: 1"))
|
||
.chain(context.filters())
|
||
.collect::<Vec<_>>();
|
||
uv_snapshot!(filters, context.install()
|
||
.arg("-r")
|
||
.arg("requirements.in")
|
||
.env("UV_NO_BUILD_ISOLATION", "yes"), @r###"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: Failed to download and build: `anyio @ https://files.pythonhosted.org/packages/db/4d/3970183622f0330d3c23d9b8a5f52e365e50381fd484d08e3285104333d3/anyio-4.3.0.tar.gz`
|
||
Caused by: Failed to build: `anyio @ https://files.pythonhosted.org/packages/db/4d/3970183622f0330d3c23d9b8a5f52e365e50381fd484d08e3285104333d3/anyio-4.3.0.tar.gz`
|
||
Caused by: Build backend failed to determine metadata through `prepare_metadata_for_build_wheel` with exit status: 1
|
||
--- stdout:
|
||
|
||
--- stderr:
|
||
Traceback (most recent call last):
|
||
File "<string>", line 8, in <module>
|
||
ModuleNotFoundError: No module named 'setuptools'
|
||
---
|
||
"###
|
||
);
|
||
|
||
// Install `setuptools` and `wheel`.
|
||
uv_snapshot!(context.install()
|
||
.arg("setuptools")
|
||
.arg("wheel"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 2 packages in [TIME]
|
||
Prepared 2 packages in [TIME]
|
||
Installed 2 packages in [TIME]
|
||
+ setuptools==69.2.0
|
||
+ wheel==0.43.0
|
||
"###);
|
||
|
||
// We expect the build to succeed, since `setuptools` is now installed.
|
||
uv_snapshot!(context.install()
|
||
.arg("-r")
|
||
.arg("requirements.in")
|
||
.env("UV_NO_BUILD_ISOLATION", "yes"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 3 packages in [TIME]
|
||
Prepared 3 packages in [TIME]
|
||
Installed 3 packages in [TIME]
|
||
+ anyio==0.0.0 (from https://files.pythonhosted.org/packages/db/4d/3970183622f0330d3c23d9b8a5f52e365e50381fd484d08e3285104333d3/anyio-4.3.0.tar.gz)
|
||
+ idna==3.6
|
||
+ sniffio==1.3.1
|
||
"###
|
||
);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// This tests that `uv` can read UTF-16LE encoded requirements.txt files.
|
||
///
|
||
/// Ref: <https://github.com/astral-sh/uv/issues/2276>
|
||
#[test]
|
||
fn install_utf16le_requirements() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||
requirements_txt.write_binary(&utf8_to_utf16_with_bom_le("tomli"))?;
|
||
|
||
uv_snapshot!(context.install_without_exclude_newer()
|
||
.arg("-r")
|
||
.arg("requirements.txt"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 1 package in [TIME]
|
||
Prepared 1 package in [TIME]
|
||
Installed 1 package in [TIME]
|
||
+ tomli==2.0.1
|
||
"###
|
||
);
|
||
Ok(())
|
||
}
|
||
|
||
/// This tests that `uv` can read UTF-16BE encoded requirements.txt files.
|
||
///
|
||
/// Ref: <https://github.com/astral-sh/uv/issues/2276>
|
||
#[test]
|
||
fn install_utf16be_requirements() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||
requirements_txt.write_binary(&utf8_to_utf16_with_bom_be("tomli"))?;
|
||
|
||
uv_snapshot!(context.install_without_exclude_newer()
|
||
.arg("-r")
|
||
.arg("requirements.txt"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 1 package in [TIME]
|
||
Prepared 1 package in [TIME]
|
||
Installed 1 package in [TIME]
|
||
+ tomli==2.0.1
|
||
"###
|
||
);
|
||
Ok(())
|
||
}
|
||
|
||
fn utf8_to_utf16_with_bom_le(s: &str) -> Vec<u8> {
|
||
use byteorder::ByteOrder;
|
||
|
||
let mut u16s = vec![0xFEFF];
|
||
u16s.extend(s.encode_utf16());
|
||
let mut u8s = vec![0; u16s.len() * 2];
|
||
byteorder::LittleEndian::write_u16_into(&u16s, &mut u8s);
|
||
u8s
|
||
}
|
||
|
||
fn utf8_to_utf16_with_bom_be(s: &str) -> Vec<u8> {
|
||
use byteorder::ByteOrder;
|
||
|
||
let mut u16s = vec![0xFEFF];
|
||
u16s.extend(s.encode_utf16());
|
||
let mut u8s = vec![0; u16s.len() * 2];
|
||
byteorder::BigEndian::write_u16_into(&u16s, &mut u8s);
|
||
u8s
|
||
}
|
||
|
||
#[test]
|
||
fn dry_run_install() -> std::result::Result<(), Box<dyn std::error::Error>> {
|
||
let context = TestContext::new("3.12");
|
||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||
requirements_txt.write_str("httpx==0.25.1")?;
|
||
|
||
uv_snapshot!(context.install()
|
||
.arg("-r")
|
||
.arg("requirements.txt")
|
||
.arg("--dry-run")
|
||
.arg("--strict"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 7 packages in [TIME]
|
||
Would download 7 packages
|
||
Would install 7 packages
|
||
+ anyio==4.3.0
|
||
+ certifi==2024.2.2
|
||
+ h11==0.14.0
|
||
+ httpcore==1.0.4
|
||
+ httpx==0.25.1
|
||
+ idna==3.6
|
||
+ sniffio==1.3.1
|
||
"###
|
||
);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn dry_run_install_url_dependency() -> std::result::Result<(), Box<dyn std::error::Error>> {
|
||
let context = TestContext::new("3.12");
|
||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||
requirements_txt.write_str("anyio @ https://files.pythonhosted.org/packages/2d/b8/7333d87d5f03247215d86a86362fd3e324111788c6cdd8d2e6196a6ba833/anyio-4.2.0.tar.gz")?;
|
||
|
||
uv_snapshot!(context.install()
|
||
.arg("-r")
|
||
.arg("requirements.txt")
|
||
.arg("--dry-run")
|
||
.arg("--strict"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 3 packages in [TIME]
|
||
Would download 3 packages
|
||
Would install 3 packages
|
||
+ anyio @ https://files.pythonhosted.org/packages/2d/b8/7333d87d5f03247215d86a86362fd3e324111788c6cdd8d2e6196a6ba833/anyio-4.2.0.tar.gz
|
||
+ idna==3.6
|
||
+ sniffio==1.3.1
|
||
"###
|
||
);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn dry_run_uninstall_url_dependency() -> std::result::Result<(), Box<dyn std::error::Error>> {
|
||
let context = TestContext::new("3.12");
|
||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||
requirements_txt.write_str("anyio @ https://files.pythonhosted.org/packages/2d/b8/7333d87d5f03247215d86a86362fd3e324111788c6cdd8d2e6196a6ba833/anyio-4.2.0.tar.gz")?;
|
||
|
||
// Install the URL dependency
|
||
uv_snapshot!(context.install()
|
||
.arg("-r")
|
||
.arg("requirements.txt")
|
||
.arg("--strict"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 3 packages in [TIME]
|
||
Prepared 3 packages in [TIME]
|
||
Installed 3 packages in [TIME]
|
||
+ anyio==4.2.0 (from https://files.pythonhosted.org/packages/2d/b8/7333d87d5f03247215d86a86362fd3e324111788c6cdd8d2e6196a6ba833/anyio-4.2.0.tar.gz)
|
||
+ idna==3.6
|
||
+ sniffio==1.3.1
|
||
"###
|
||
);
|
||
|
||
// Then switch to a registry dependency
|
||
requirements_txt.write_str("anyio")?;
|
||
uv_snapshot!(context.install()
|
||
.arg("-r")
|
||
.arg("requirements.txt")
|
||
.arg("--upgrade-package")
|
||
.arg("anyio")
|
||
.arg("--dry-run")
|
||
.arg("--strict"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 3 packages in [TIME]
|
||
Would download 1 package
|
||
Would uninstall 1 package
|
||
Would install 1 package
|
||
- anyio==4.2.0 (from https://files.pythonhosted.org/packages/2d/b8/7333d87d5f03247215d86a86362fd3e324111788c6cdd8d2e6196a6ba833/anyio-4.2.0.tar.gz)
|
||
+ anyio==4.3.0
|
||
"###
|
||
);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn dry_run_install_already_installed() -> std::result::Result<(), Box<dyn std::error::Error>> {
|
||
let context = TestContext::new("3.12");
|
||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||
requirements_txt.write_str("httpx==0.25.1")?;
|
||
|
||
// Install the package
|
||
uv_snapshot!(context.install()
|
||
.arg("-r")
|
||
.arg("requirements.txt")
|
||
.arg("--strict"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 7 packages in [TIME]
|
||
Prepared 7 packages in [TIME]
|
||
Installed 7 packages in [TIME]
|
||
+ anyio==4.3.0
|
||
+ certifi==2024.2.2
|
||
+ h11==0.14.0
|
||
+ httpcore==1.0.4
|
||
+ httpx==0.25.1
|
||
+ idna==3.6
|
||
+ sniffio==1.3.1
|
||
"###
|
||
);
|
||
|
||
// Install again with dry run enabled
|
||
uv_snapshot!(context.install()
|
||
.arg("-r")
|
||
.arg("requirements.txt")
|
||
.arg("--dry-run")
|
||
.arg("--strict"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Audited 1 package in [TIME]
|
||
Would make no changes
|
||
"###
|
||
);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn dry_run_install_transitive_dependency_already_installed(
|
||
) -> std::result::Result<(), Box<dyn std::error::Error>> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||
requirements_txt.write_str("httpcore==1.0.2")?;
|
||
|
||
// Install a dependency of httpx
|
||
uv_snapshot!(context.install()
|
||
.arg("-r")
|
||
.arg("requirements.txt")
|
||
.arg("--strict"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 3 packages in [TIME]
|
||
Prepared 3 packages in [TIME]
|
||
Installed 3 packages in [TIME]
|
||
+ certifi==2024.2.2
|
||
+ h11==0.14.0
|
||
+ httpcore==1.0.2
|
||
"###
|
||
);
|
||
|
||
// Install it httpx with dry run enabled
|
||
requirements_txt.write_str("httpx==0.25.1")?;
|
||
uv_snapshot!(context.install()
|
||
.arg("-r")
|
||
.arg("requirements.txt")
|
||
.arg("--dry-run")
|
||
.arg("--strict"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 7 packages in [TIME]
|
||
Would download 4 packages
|
||
Would install 4 packages
|
||
+ anyio==4.3.0
|
||
+ httpx==0.25.1
|
||
+ idna==3.6
|
||
+ sniffio==1.3.1
|
||
"###
|
||
);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn dry_run_install_then_upgrade() -> std::result::Result<(), Box<dyn std::error::Error>> {
|
||
let context = TestContext::new("3.12");
|
||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||
requirements_txt.write_str("httpx==0.25.0")?;
|
||
|
||
// Install the package
|
||
uv_snapshot!(context.install()
|
||
.arg("-r")
|
||
.arg("requirements.txt")
|
||
.arg("--strict"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 7 packages in [TIME]
|
||
Prepared 7 packages in [TIME]
|
||
Installed 7 packages in [TIME]
|
||
+ anyio==4.3.0
|
||
+ certifi==2024.2.2
|
||
+ h11==0.14.0
|
||
+ httpcore==0.18.0
|
||
+ httpx==0.25.0
|
||
+ idna==3.6
|
||
+ sniffio==1.3.1
|
||
"###
|
||
);
|
||
|
||
// Bump the version and install with dry run enabled
|
||
requirements_txt.write_str("httpx==0.25.1")?;
|
||
uv_snapshot!(context.install()
|
||
.arg("-r")
|
||
.arg("requirements.txt")
|
||
.arg("--dry-run"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 7 packages in [TIME]
|
||
Would download 1 package
|
||
Would uninstall 1 package
|
||
Would install 1 package
|
||
- httpx==0.25.0
|
||
+ httpx==0.25.1
|
||
"###
|
||
);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Raise an error when a direct URL's `Requires-Python` constraint is not met.
|
||
#[test]
|
||
fn requires_python_direct_url() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
// Create an editable package with a `Requires-Python` constraint that is not met.
|
||
let editable_dir = context.temp_dir.child("editable");
|
||
editable_dir.create_dir_all()?;
|
||
let pyproject_toml = editable_dir.child("pyproject.toml");
|
||
pyproject_toml.write_str(
|
||
r#"[project]
|
||
name = "example"
|
||
version = "0.0.0"
|
||
dependencies = [
|
||
"anyio==4.0.0"
|
||
]
|
||
requires-python = "<=3.8"
|
||
"#,
|
||
)?;
|
||
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg(format!("example @ {}", editable_dir.path().display())), @r###"
|
||
success: false
|
||
exit_code: 1
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
× No solution found when resolving dependencies:
|
||
╰─▶ Because the current Python version (3.12.[X]) does not satisfy Python<=3.8 and example==0.0.0 depends on Python<=3.8, we can conclude that example==0.0.0 cannot be used.
|
||
And because only example==0.0.0 is available and you require example, we can conclude that the requirements are unsatisfiable.
|
||
"###
|
||
);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Install a package from an index that requires authentication
|
||
#[test]
|
||
fn install_package_basic_auth_from_url() {
|
||
let context = TestContext::new("3.12");
|
||
|
||
uv_snapshot!(context.install()
|
||
.arg("anyio")
|
||
.arg("--index-url")
|
||
.arg("https://public:heron@pypi-proxy.fly.dev/basic-auth/simple")
|
||
.arg("--strict"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 3 packages in [TIME]
|
||
Prepared 3 packages in [TIME]
|
||
Installed 3 packages in [TIME]
|
||
+ anyio==4.3.0
|
||
+ idna==3.6
|
||
+ sniffio==1.3.1
|
||
"###
|
||
);
|
||
|
||
context.assert_command("import anyio").success();
|
||
}
|
||
|
||
/// Install a package from an index that requires authentication
|
||
#[test]
|
||
fn install_package_basic_auth_from_netrc_default() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
let netrc = context.temp_dir.child(".netrc");
|
||
netrc.write_str("default login public password heron")?;
|
||
|
||
uv_snapshot!(context.install()
|
||
.arg("anyio")
|
||
.arg("--index-url")
|
||
.arg("https://pypi-proxy.fly.dev/basic-auth/simple")
|
||
.env("NETRC", netrc.to_str().unwrap())
|
||
.arg("--strict"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 3 packages in [TIME]
|
||
Prepared 3 packages in [TIME]
|
||
Installed 3 packages in [TIME]
|
||
+ anyio==4.3.0
|
||
+ idna==3.6
|
||
+ sniffio==1.3.1
|
||
"###
|
||
);
|
||
|
||
context.assert_command("import anyio").success();
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Install a package from an index that requires authentication
|
||
#[test]
|
||
fn install_package_basic_auth_from_netrc() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
let netrc = context.temp_dir.child(".netrc");
|
||
netrc.write_str("machine pypi-proxy.fly.dev login public password heron")?;
|
||
|
||
uv_snapshot!(context.install()
|
||
.arg("anyio")
|
||
.arg("--index-url")
|
||
.arg("https://pypi-proxy.fly.dev/basic-auth/simple")
|
||
.env("NETRC", netrc.to_str().unwrap())
|
||
.arg("--strict"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 3 packages in [TIME]
|
||
Prepared 3 packages in [TIME]
|
||
Installed 3 packages in [TIME]
|
||
+ anyio==4.3.0
|
||
+ idna==3.6
|
||
+ sniffio==1.3.1
|
||
"###
|
||
);
|
||
|
||
context.assert_command("import anyio").success();
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Install a package from an index that requires authentication
|
||
/// Define the `--index-url` in the requirements file
|
||
#[test]
|
||
fn install_package_basic_auth_from_netrc_index_in_requirements() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
let netrc = context.temp_dir.child(".netrc");
|
||
netrc.write_str("machine pypi-proxy.fly.dev login public password heron")?;
|
||
|
||
let requirements = context.temp_dir.child("requirements.txt");
|
||
requirements.write_str(
|
||
r"
|
||
anyio
|
||
--index-url https://pypi-proxy.fly.dev/basic-auth/simple
|
||
",
|
||
)?;
|
||
|
||
uv_snapshot!(context.install()
|
||
.arg("-r")
|
||
.arg("requirements.txt")
|
||
.env("NETRC", netrc.to_str().unwrap())
|
||
.arg("--strict"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 3 packages in [TIME]
|
||
Prepared 3 packages in [TIME]
|
||
Installed 3 packages in [TIME]
|
||
+ anyio==4.3.0
|
||
+ idna==3.6
|
||
+ sniffio==1.3.1
|
||
"###
|
||
);
|
||
|
||
context.assert_command("import anyio").success();
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Install a package from an index that provides relative links
|
||
#[test]
|
||
fn install_index_with_relative_links() {
|
||
let context = TestContext::new("3.12");
|
||
|
||
uv_snapshot!(context.install()
|
||
.arg("anyio")
|
||
.arg("--index-url")
|
||
.arg("https://pypi-proxy.fly.dev/relative/simple")
|
||
.arg("--strict"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 3 packages in [TIME]
|
||
Prepared 3 packages in [TIME]
|
||
Installed 3 packages in [TIME]
|
||
+ anyio==4.3.0
|
||
+ idna==3.6
|
||
+ sniffio==1.3.1
|
||
"###
|
||
);
|
||
|
||
context.assert_command("import anyio").success();
|
||
}
|
||
|
||
/// Install a package from an index that requires authentication from the keyring.
|
||
#[test]
|
||
fn install_package_basic_auth_from_keyring() {
|
||
let context = TestContext::new("3.12");
|
||
|
||
// Install our keyring plugin
|
||
context
|
||
.install()
|
||
.arg(
|
||
context
|
||
.workspace_root
|
||
.join("scripts")
|
||
.join("packages")
|
||
.join("keyring_test_plugin"),
|
||
)
|
||
.assert()
|
||
.success();
|
||
|
||
uv_snapshot!(context.install()
|
||
.arg("anyio")
|
||
.arg("--index-url")
|
||
.arg("https://public@pypi-proxy.fly.dev/basic-auth/simple")
|
||
.arg("--keyring-provider")
|
||
.arg("subprocess")
|
||
.arg("--strict")
|
||
.env("KEYRING_TEST_CREDENTIALS", r#"{"pypi-proxy.fly.dev": {"public": "heron"}}"#)
|
||
.env("PATH", venv_bin_path(&context.venv)), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Request for public@https://pypi-proxy.fly.dev/basic-auth/simple/anyio/
|
||
Request for public@pypi-proxy.fly.dev
|
||
Resolved 3 packages in [TIME]
|
||
Prepared 3 packages in [TIME]
|
||
Installed 3 packages in [TIME]
|
||
+ anyio==4.3.0
|
||
+ idna==3.6
|
||
+ sniffio==1.3.1
|
||
"###
|
||
);
|
||
|
||
context.assert_command("import anyio").success();
|
||
}
|
||
|
||
/// Install a package from an index that requires authentication
|
||
/// but the keyring has the wrong password
|
||
#[test]
|
||
fn install_package_basic_auth_from_keyring_wrong_password() {
|
||
let context = TestContext::new("3.12");
|
||
|
||
// Install our keyring plugin
|
||
context
|
||
.install()
|
||
.arg(
|
||
context
|
||
.workspace_root
|
||
.join("scripts")
|
||
.join("packages")
|
||
.join("keyring_test_plugin"),
|
||
)
|
||
.assert()
|
||
.success();
|
||
|
||
uv_snapshot!(context.install()
|
||
.arg("anyio")
|
||
.arg("--index-url")
|
||
.arg("https://public@pypi-proxy.fly.dev/basic-auth/simple")
|
||
.arg("--keyring-provider")
|
||
.arg("subprocess")
|
||
.arg("--strict")
|
||
.env("KEYRING_TEST_CREDENTIALS", r#"{"pypi-proxy.fly.dev": {"public": "foobar"}}"#)
|
||
.env("PATH", venv_bin_path(&context.venv)), @r###"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Request for public@https://pypi-proxy.fly.dev/basic-auth/simple/anyio/
|
||
Request for public@pypi-proxy.fly.dev
|
||
error: Failed to download `anyio==4.3.0`
|
||
Caused by: HTTP status client error (401 Unauthorized) for url (https://pypi-proxy.fly.dev/basic-auth/files/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl.metadata)
|
||
"###
|
||
);
|
||
}
|
||
|
||
/// Install a package from an index that requires authentication
|
||
/// but the keyring has the wrong username
|
||
#[test]
|
||
fn install_package_basic_auth_from_keyring_wrong_username() {
|
||
let context = TestContext::new("3.12");
|
||
|
||
// Install our keyring plugin
|
||
context
|
||
.install()
|
||
.arg(
|
||
context
|
||
.workspace_root
|
||
.join("scripts")
|
||
.join("packages")
|
||
.join("keyring_test_plugin"),
|
||
)
|
||
.assert()
|
||
.success();
|
||
|
||
uv_snapshot!(context.install()
|
||
.arg("anyio")
|
||
.arg("--index-url")
|
||
.arg("https://public@pypi-proxy.fly.dev/basic-auth/simple")
|
||
.arg("--keyring-provider")
|
||
.arg("subprocess")
|
||
.arg("--strict")
|
||
.env("KEYRING_TEST_CREDENTIALS", r#"{"pypi-proxy.fly.dev": {"other": "heron"}}"#)
|
||
.env("PATH", venv_bin_path(&context.venv)), @r###"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Request for public@https://pypi-proxy.fly.dev/basic-auth/simple/anyio/
|
||
Request for public@pypi-proxy.fly.dev
|
||
error: Failed to download `anyio==4.3.0`
|
||
Caused by: HTTP status client error (401 Unauthorized) for url (https://pypi-proxy.fly.dev/basic-auth/files/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl.metadata)
|
||
"###
|
||
);
|
||
}
|
||
|
||
/// Install a package from an index that provides relative links and requires authentication
|
||
#[test]
|
||
fn install_index_with_relative_links_authenticated() {
|
||
let context = TestContext::new("3.12");
|
||
|
||
uv_snapshot!(context.install()
|
||
.arg("anyio")
|
||
.arg("--index-url")
|
||
.arg("https://public:heron@pypi-proxy.fly.dev/basic-auth/relative/simple")
|
||
.arg("--strict"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 3 packages in [TIME]
|
||
Prepared 3 packages in [TIME]
|
||
Installed 3 packages in [TIME]
|
||
+ anyio==4.3.0
|
||
+ idna==3.6
|
||
+ sniffio==1.3.1
|
||
"###
|
||
);
|
||
|
||
context.assert_command("import anyio").success();
|
||
}
|
||
|
||
/// The modified time of `site-packages` should change on package installation.
|
||
#[cfg(unix)]
|
||
#[test]
|
||
fn install_site_packages_mtime_updated() -> Result<()> {
|
||
use std::os::unix::fs::MetadataExt;
|
||
|
||
let context = TestContext::new("3.12");
|
||
|
||
let site_packages = context.site_packages();
|
||
|
||
// `mtime` is only second-resolution so we include the nanoseconds as well
|
||
let metadata = site_packages.metadata()?;
|
||
let pre_mtime = metadata.mtime();
|
||
let pre_mtime_ns = metadata.mtime_nsec();
|
||
|
||
// Install a package.
|
||
uv_snapshot!(context.install()
|
||
.arg("anyio")
|
||
.arg("--strict"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 3 packages in [TIME]
|
||
Prepared 3 packages in [TIME]
|
||
Installed 3 packages in [TIME]
|
||
+ anyio==4.3.0
|
||
+ idna==3.6
|
||
+ sniffio==1.3.1
|
||
"###
|
||
);
|
||
|
||
let metadata = site_packages.metadata()?;
|
||
let post_mtime = metadata.mtime();
|
||
let post_mtime_ns = metadata.mtime_nsec();
|
||
|
||
assert!(
|
||
(post_mtime, post_mtime_ns) > (pre_mtime, pre_mtime_ns),
|
||
"Expected newer mtime than {pre_mtime}.{pre_mtime_ns} but got {post_mtime}.{post_mtime_ns}"
|
||
);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// We had a bug where maturin would walk up to the top level gitignore of the cache with a `*`
|
||
/// entry (because we want to ignore the entire cache from outside), ignoring all python source
|
||
/// files.
|
||
#[test]
|
||
fn deptry_gitignore() {
|
||
let context = TestContext::new("3.12");
|
||
|
||
let source_dist_dir = context
|
||
.workspace_root
|
||
.join("scripts/packages/deptry_reproducer");
|
||
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg(format!("deptry_reproducer @ {}", source_dist_dir.join("deptry_reproducer-0.1.0.tar.gz").simplified_display()))
|
||
.arg("--strict")
|
||
.current_dir(source_dist_dir), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 3 packages in [TIME]
|
||
Prepared 3 packages in [TIME]
|
||
Installed 3 packages in [TIME]
|
||
+ cffi==1.16.0
|
||
+ deptry-reproducer==0.1.0 (from file://[WORKSPACE]/scripts/packages/deptry_reproducer/deptry_reproducer-0.1.0.tar.gz)
|
||
+ pycparser==2.21
|
||
"###
|
||
);
|
||
|
||
// Check that we packed the python source files
|
||
context
|
||
.assert_command("import deptry_reproducer.foo")
|
||
.success();
|
||
}
|
||
|
||
/// Reinstall an installed package with `--no-index`
|
||
#[test]
|
||
fn reinstall_no_index() {
|
||
let context = TestContext::new("3.12");
|
||
|
||
// Install anyio
|
||
uv_snapshot!(context.install()
|
||
.arg("anyio")
|
||
.arg("--strict"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 3 packages in [TIME]
|
||
Prepared 3 packages in [TIME]
|
||
Installed 3 packages in [TIME]
|
||
+ anyio==4.3.0
|
||
+ idna==3.6
|
||
+ sniffio==1.3.1
|
||
"###
|
||
);
|
||
|
||
// Install anyio again
|
||
uv_snapshot!(context.install()
|
||
.arg("anyio")
|
||
.arg("--no-index")
|
||
.arg("--strict"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Audited 1 package in [TIME]
|
||
"###
|
||
);
|
||
|
||
// Reinstall
|
||
// We should not consider the already installed package as a source and
|
||
// should attempt to pull from the index
|
||
uv_snapshot!(context.install()
|
||
.arg("anyio")
|
||
.arg("--no-index")
|
||
.arg("--reinstall")
|
||
.arg("--strict"), @r###"
|
||
success: false
|
||
exit_code: 1
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
× No solution found when resolving dependencies:
|
||
╰─▶ Because anyio was not found in the provided package locations and you require anyio, we can conclude that the requirements are unsatisfiable.
|
||
|
||
hint: Packages were unavailable because index lookups were disabled and no additional package locations were provided (try: `--find-links <uri>`)
|
||
"###
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn already_installed_remote_dependencies() {
|
||
let context = TestContext::new("3.12");
|
||
|
||
// Install anyio's dependencies.
|
||
uv_snapshot!(context.install()
|
||
.arg("idna")
|
||
.arg("sniffio")
|
||
.arg("--strict"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 2 packages in [TIME]
|
||
Prepared 2 packages in [TIME]
|
||
Installed 2 packages in [TIME]
|
||
+ idna==3.6
|
||
+ sniffio==1.3.1
|
||
"###
|
||
);
|
||
|
||
// Install anyio.
|
||
uv_snapshot!(context.install()
|
||
.arg("anyio")
|
||
.arg("--strict"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 3 packages in [TIME]
|
||
Prepared 1 package in [TIME]
|
||
Installed 1 package in [TIME]
|
||
+ anyio==4.3.0
|
||
"###
|
||
);
|
||
}
|
||
|
||
/// Install an editable package that depends on a previously installed editable package.
|
||
#[test]
|
||
fn already_installed_dependent_editable() {
|
||
let context = TestContext::new("3.12");
|
||
let root_path = context
|
||
.workspace_root
|
||
.join("scripts/packages/dependent_locals");
|
||
|
||
// Install the first editable
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg("-e")
|
||
.arg(root_path.join("first_local")), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 1 package in [TIME]
|
||
Prepared 1 package in [TIME]
|
||
Installed 1 package in [TIME]
|
||
+ first-local==0.1.0 (from file://[WORKSPACE]/scripts/packages/dependent_locals/first_local)
|
||
"###
|
||
);
|
||
|
||
// Install the second editable which depends on the first editable
|
||
// The already installed first editable package should satisfy the requirement
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg("-e")
|
||
.arg(root_path.join("second_local"))
|
||
// Disable the index to guard this test against dependency confusion attacks
|
||
.arg("--no-index")
|
||
.arg("--find-links")
|
||
.arg(BUILD_VENDOR_LINKS_URL), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 2 packages in [TIME]
|
||
Prepared 1 package in [TIME]
|
||
Installed 1 package in [TIME]
|
||
+ second-local==0.1.0 (from file://[WORKSPACE]/scripts/packages/dependent_locals/second_local)
|
||
"###
|
||
);
|
||
|
||
// Request install of the first editable by full path again
|
||
// We should audit the installed package
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg("-e")
|
||
.arg(root_path.join("first_local")), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Audited 1 package in [TIME]
|
||
"###
|
||
);
|
||
|
||
// Request reinstallation of the first package during install of the second
|
||
// It's not available on an index and the user has not specified the path so we fail.
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg("-e")
|
||
.arg(root_path.join("second_local"))
|
||
.arg("--reinstall-package")
|
||
.arg("first-local")
|
||
// Disable the index to guard this test against dependency confusion attacks
|
||
.arg("--no-index")
|
||
.arg("--find-links")
|
||
.arg(BUILD_VENDOR_LINKS_URL), @r###"
|
||
success: false
|
||
exit_code: 1
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
× No solution found when resolving dependencies:
|
||
╰─▶ Because first-local was not found in the provided package locations and second-local==0.1.0 depends on first-local, we can conclude that second-local==0.1.0 cannot be used.
|
||
And because only second-local==0.1.0 is available and you require second-local, we can conclude that the requirements are unsatisfiable.
|
||
"###
|
||
);
|
||
|
||
// Request reinstallation of the first package
|
||
// We include it in the install command with a full path so we should succeed
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg("-e")
|
||
.arg(root_path.join("first_local"))
|
||
.arg("--reinstall-package")
|
||
.arg("first-local"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 1 package in [TIME]
|
||
Prepared 1 package in [TIME]
|
||
Uninstalled 1 package in [TIME]
|
||
Installed 1 package in [TIME]
|
||
- first-local==0.1.0 (from file://[WORKSPACE]/scripts/packages/dependent_locals/first_local)
|
||
+ first-local==0.1.0 (from file://[WORKSPACE]/scripts/packages/dependent_locals/first_local)
|
||
"###
|
||
);
|
||
}
|
||
|
||
/// Install a local package that depends on a previously installed local package.
|
||
#[test]
|
||
fn already_installed_local_path_dependent() {
|
||
let context = TestContext::new("3.12");
|
||
let root_path = context
|
||
.workspace_root
|
||
.join("scripts/packages/dependent_locals");
|
||
|
||
// Install the first local
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg(root_path.join("first_local")), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 1 package in [TIME]
|
||
Prepared 1 package in [TIME]
|
||
Installed 1 package in [TIME]
|
||
+ first-local==0.1.0 (from file://[WORKSPACE]/scripts/packages/dependent_locals/first_local)
|
||
"###
|
||
);
|
||
|
||
// Install the second local which depends on the first local
|
||
// The already installed first local package should satisfy the requirement
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg(root_path.join("second_local"))
|
||
// Disable the index to guard this test against dependency confusion attacks
|
||
.arg("--no-index")
|
||
.arg("--find-links")
|
||
.arg(BUILD_VENDOR_LINKS_URL), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 2 packages in [TIME]
|
||
Prepared 1 package in [TIME]
|
||
Installed 1 package in [TIME]
|
||
+ second-local==0.1.0 (from file://[WORKSPACE]/scripts/packages/dependent_locals/second_local)
|
||
"###
|
||
);
|
||
|
||
// Request install of the first local by full path again
|
||
// We should audit the installed package
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg(root_path.join("first_local")), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Audited 1 package in [TIME]
|
||
"###
|
||
);
|
||
|
||
// Request reinstallation of the first package during install of the second
|
||
// It's not available on an index and the user has not specified the path so we fail
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg(root_path.join("second_local"))
|
||
.arg("--reinstall-package")
|
||
.arg("first-local")
|
||
// Disable the index to guard this test against dependency confusion attacks
|
||
.arg("--no-index")
|
||
.arg("--find-links")
|
||
.arg(BUILD_VENDOR_LINKS_URL), @r###"
|
||
success: false
|
||
exit_code: 1
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
× No solution found when resolving dependencies:
|
||
╰─▶ Because first-local was not found in the provided package locations and second-local==0.1.0 depends on first-local, we can conclude that second-local==0.1.0 cannot be used.
|
||
And because only second-local==0.1.0 is available and you require second-local, we can conclude that the requirements are unsatisfiable.
|
||
"###
|
||
);
|
||
|
||
// Request reinstallation of the first package
|
||
// We include it in the install command with a full path so we succeed
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg(root_path.join("second_local"))
|
||
.arg(root_path.join("first_local"))
|
||
.arg("--reinstall-package")
|
||
.arg("first-local"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 2 packages in [TIME]
|
||
Prepared 1 package in [TIME]
|
||
Uninstalled 1 package in [TIME]
|
||
Installed 1 package in [TIME]
|
||
- first-local==0.1.0 (from file://[WORKSPACE]/scripts/packages/dependent_locals/first_local)
|
||
+ first-local==0.1.0 (from file://[WORKSPACE]/scripts/packages/dependent_locals/first_local)
|
||
"###
|
||
);
|
||
|
||
// Request upgrade of the first package
|
||
// It's not available on an index and the user has not specified the path so we fail
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg(root_path.join("second_local"))
|
||
.arg("--upgrade-package")
|
||
.arg("first-local")
|
||
// Disable the index to guard this test against dependency confusion attacks
|
||
.arg("--no-index")
|
||
.arg("--find-links")
|
||
.arg(BUILD_VENDOR_LINKS_URL), @r###"
|
||
success: false
|
||
exit_code: 1
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
× No solution found when resolving dependencies:
|
||
╰─▶ Because first-local was not found in the provided package locations and second-local==0.1.0 depends on first-local, we can conclude that second-local==0.1.0 cannot be used.
|
||
And because only second-local==0.1.0 is available and you require second-local, we can conclude that the requirements are unsatisfiable.
|
||
"###
|
||
);
|
||
|
||
// Request upgrade of the first package
|
||
// A full path is specified and there's nothing to upgrade to so we should just audit
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg(root_path.join("first_local"))
|
||
.arg(root_path.join("second_local"))
|
||
.arg("--upgrade-package")
|
||
.arg("first-local")
|
||
// Disable the index to guard this test against dependency confusion attacks
|
||
.arg("--no-index")
|
||
.arg("--find-links")
|
||
.arg(BUILD_VENDOR_LINKS_URL), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 2 packages in [TIME]
|
||
Audited 2 packages in [TIME]
|
||
"###
|
||
);
|
||
}
|
||
|
||
/// A local version of a package shadowing a remote package is installed.
|
||
#[test]
|
||
fn already_installed_local_version_of_remote_package() {
|
||
let context = TestContext::new("3.12");
|
||
let root_path = context.workspace_root.join("scripts/packages");
|
||
|
||
// Install the local anyio first
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg(root_path.join("anyio_local")), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 1 package in [TIME]
|
||
Prepared 1 package in [TIME]
|
||
Installed 1 package in [TIME]
|
||
+ anyio==4.3.0+foo (from file://[WORKSPACE]/scripts/packages/anyio_local)
|
||
"###
|
||
);
|
||
|
||
// Install again without specifying a local path — this should not pull from the index
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg("anyio"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Audited 1 package in [TIME]
|
||
"###
|
||
);
|
||
|
||
// Request install with a different version
|
||
// We should attempt to pull from the index since the installed version does not match
|
||
// but we disable it here to preserve this dependency for future tests
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg("anyio==4.2.0")
|
||
.arg("--no-index"), @r###"
|
||
success: false
|
||
exit_code: 1
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
× No solution found when resolving dependencies:
|
||
╰─▶ Because anyio was not found in the provided package locations and you require anyio==4.2.0, we can conclude that the requirements are unsatisfiable.
|
||
|
||
hint: Packages were unavailable because index lookups were disabled and no additional package locations were provided (try: `--find-links <uri>`)
|
||
"###
|
||
);
|
||
|
||
// Request reinstallation with the local version segment — this should fail since it is not available
|
||
// in the index and the path was not provided
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg("anyio==4.3.0+foo")
|
||
.arg("--reinstall"), @r###"
|
||
success: false
|
||
exit_code: 1
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
× No solution found when resolving dependencies:
|
||
╰─▶ Because there is no version of anyio==4.3.0+foo and you require anyio==4.3.0+foo, we can conclude that the requirements are unsatisfiable.
|
||
"###
|
||
);
|
||
|
||
// Request reinstall with the full path, this should reinstall from the path
|
||
// and not pull from the index
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg(root_path.join("anyio_local"))
|
||
.arg("--reinstall")
|
||
.arg("anyio"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 1 package in [TIME]
|
||
Prepared 1 package in [TIME]
|
||
Uninstalled 1 package in [TIME]
|
||
Installed 1 package in [TIME]
|
||
- anyio==4.3.0+foo (from file://[WORKSPACE]/scripts/packages/anyio_local)
|
||
+ anyio==4.3.0+foo (from file://[WORKSPACE]/scripts/packages/anyio_local)
|
||
"###
|
||
);
|
||
|
||
// Request reinstallation with just the name, this should pull from the index
|
||
// and replace the path dependency
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg("anyio")
|
||
.arg("--reinstall"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 3 packages in [TIME]
|
||
Prepared 3 packages in [TIME]
|
||
Uninstalled 1 package in [TIME]
|
||
Installed 3 packages in [TIME]
|
||
- anyio==4.3.0+foo (from file://[WORKSPACE]/scripts/packages/anyio_local)
|
||
+ anyio==4.3.0
|
||
+ idna==3.6
|
||
+ sniffio==1.3.1
|
||
"###
|
||
);
|
||
|
||
// Install the local anyio again so we can test upgrades
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg(root_path.join("anyio_local")), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 1 package in [TIME]
|
||
Prepared 1 package in [TIME]
|
||
Uninstalled 1 package in [TIME]
|
||
Installed 1 package in [TIME]
|
||
- anyio==4.3.0
|
||
+ anyio==4.3.0+foo (from file://[WORKSPACE]/scripts/packages/anyio_local)
|
||
"###
|
||
);
|
||
|
||
// Request upgrade with just the name
|
||
// We shouldn't pull from the index because the local version is "newer"
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg("anyio")
|
||
.arg("--upgrade"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 3 packages in [TIME]
|
||
Audited 3 packages in [TIME]
|
||
"###
|
||
);
|
||
|
||
// Install something that depends on anyio
|
||
// We shouldn't overwrite our local version with the remote anyio here
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg("httpx"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 7 packages in [TIME]
|
||
Prepared 4 packages in [TIME]
|
||
Installed 4 packages in [TIME]
|
||
+ certifi==2024.2.2
|
||
+ h11==0.14.0
|
||
+ httpcore==1.0.4
|
||
+ httpx==0.27.0
|
||
"###
|
||
);
|
||
}
|
||
|
||
/// Install a package with multiple installed distributions in a virtual environment.
|
||
#[test]
|
||
#[cfg(unix)]
|
||
fn already_installed_multiple_versions() -> Result<()> {
|
||
fn prepare(context: &TestContext) -> Result<()> {
|
||
use crate::common::copy_dir_all;
|
||
|
||
// Install into the base environment
|
||
context.install().arg("anyio==3.7.0").assert().success();
|
||
|
||
// Install another version into another environment
|
||
let context_duplicate = TestContext::new("3.12");
|
||
context_duplicate
|
||
.install()
|
||
.arg("anyio==4.0.0")
|
||
.assert()
|
||
.success();
|
||
|
||
// Copy the second version into the first environment
|
||
copy_dir_all(
|
||
context_duplicate
|
||
.site_packages()
|
||
.join("anyio-4.0.0.dist-info"),
|
||
context.site_packages().join("anyio-4.0.0.dist-info"),
|
||
)?;
|
||
|
||
Ok(())
|
||
}
|
||
|
||
let context = TestContext::new("3.12");
|
||
|
||
prepare(&context)?;
|
||
|
||
// Request the second anyio version again
|
||
// Should remove both previous versions and reinstall the second one
|
||
uv_snapshot!(context.filters(), context.install().arg("anyio==4.0.0"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 3 packages in [TIME]
|
||
Prepared 1 package in [TIME]
|
||
Uninstalled 2 packages in [TIME]
|
||
Installed 1 package in [TIME]
|
||
- anyio==3.7.0
|
||
- anyio==4.0.0
|
||
+ anyio==4.0.0
|
||
"###
|
||
);
|
||
|
||
// Reset the test context
|
||
prepare(&context)?;
|
||
|
||
// Request the anyio without a version specifier
|
||
// This is loosely a regression test for the ordering of the installation preferences
|
||
// from existing site-packages
|
||
uv_snapshot!(context.filters(), context.install().arg("anyio"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 3 packages in [TIME]
|
||
Prepared 1 package in [TIME]
|
||
Uninstalled 2 packages in [TIME]
|
||
Installed 1 package in [TIME]
|
||
- anyio==3.7.0
|
||
- anyio==4.0.0
|
||
+ anyio==4.3.0
|
||
"###
|
||
);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Install a package from a remote URL
|
||
#[test]
|
||
#[cfg(feature = "git")]
|
||
fn already_installed_remote_url() {
|
||
let context = TestContext::new("3.8");
|
||
|
||
// First, install from the remote URL
|
||
uv_snapshot!(context.filters(), context.install().arg("uv-public-pypackage @ git+https://github.com/astral-test/uv-public-pypackage"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 1 package in [TIME]
|
||
Prepared 1 package in [TIME]
|
||
Installed 1 package in [TIME]
|
||
+ uv-public-pypackage==0.1.0 (from git+https://github.com/astral-test/uv-public-pypackage@b270df1a2fb5d012294e9aaf05e7e0bab1e6a389)
|
||
"###);
|
||
|
||
context.assert_installed("uv_public_pypackage", "0.1.0");
|
||
|
||
// Request installation again with a different URL, but the same _canonical_ URL. We should
|
||
// resolve the package (since we installed a specific commit, but are now requesting the default
|
||
// branch), but not reinstall the package.
|
||
uv_snapshot!(context.filters(), context.install().arg("uv-public-pypackage @ git+https://github.com/astral-test/uv-public-pypackage.git"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 1 package in [TIME]
|
||
Audited 1 package in [TIME]
|
||
"###);
|
||
|
||
// Request installation again with a different URL, but the same _canonical_ URL and the same
|
||
// commit. We should neither resolve nor reinstall the package, since it's already installed
|
||
// at this precise commit.
|
||
uv_snapshot!(context.filters(), context.install().arg("uv-public-pypackage @ git+https://github.com/astral-test/uv-public-pypackage.git@b270df1a2fb5d012294e9aaf05e7e0bab1e6a389"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Audited 1 package in [TIME]
|
||
"###);
|
||
|
||
// Request installation again with just the name
|
||
// We should just audit the URL package since it fulfills this requirement
|
||
uv_snapshot!(
|
||
context.install().arg("uv-public-pypackage"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Audited 1 package in [TIME]
|
||
"###);
|
||
|
||
// Request reinstallation
|
||
// We should fail since the URL was not provided
|
||
uv_snapshot!(
|
||
context.install()
|
||
.arg("uv-public-pypackage")
|
||
.arg("--no-index")
|
||
.arg("--reinstall"), @r###"
|
||
success: false
|
||
exit_code: 1
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
× No solution found when resolving dependencies:
|
||
╰─▶ Because uv-public-pypackage was not found in the provided package locations and you require uv-public-pypackage, we can conclude that the requirements are unsatisfiable.
|
||
|
||
hint: Packages were unavailable because index lookups were disabled and no additional package locations were provided (try: `--find-links <uri>`)
|
||
"###);
|
||
|
||
// Request installation again with just the full URL
|
||
// We should just audit the existing package
|
||
uv_snapshot!(
|
||
context.install().arg("uv-public-pypackage @ git+https://github.com/astral-test/uv-public-pypackage"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 1 package in [TIME]
|
||
Audited 1 package in [TIME]
|
||
"###);
|
||
|
||
// Request reinstallation with the full URL
|
||
// We should reinstall successfully
|
||
uv_snapshot!(
|
||
context.install()
|
||
.arg("uv-public-pypackage @ git+https://github.com/astral-test/uv-public-pypackage")
|
||
.arg("--reinstall"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 1 package in [TIME]
|
||
Uninstalled 1 package in [TIME]
|
||
Installed 1 package in [TIME]
|
||
- uv-public-pypackage==0.1.0 (from git+https://github.com/astral-test/uv-public-pypackage@b270df1a2fb5d012294e9aaf05e7e0bab1e6a389)
|
||
+ uv-public-pypackage==0.1.0 (from git+https://github.com/astral-test/uv-public-pypackage@b270df1a2fb5d012294e9aaf05e7e0bab1e6a389)
|
||
"###);
|
||
|
||
// Request installation again with a different version
|
||
// We should attempt to pull from the index since the local version does not match
|
||
uv_snapshot!(
|
||
context.install().arg("uv-public-pypackage==0.2.0").arg("--no-index"), @r###"
|
||
success: false
|
||
exit_code: 1
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
× No solution found when resolving dependencies:
|
||
╰─▶ Because uv-public-pypackage was not found in the provided package locations and you require uv-public-pypackage==0.2.0, we can conclude that the requirements are unsatisfiable.
|
||
|
||
hint: Packages were unavailable because index lookups were disabled and no additional package locations were provided (try: `--find-links <uri>`)
|
||
"###);
|
||
}
|
||
|
||
/// Sync using `--find-links` with a local directory.
|
||
#[test]
|
||
fn find_links() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||
requirements_txt.write_str(indoc! {r"
|
||
tqdm
|
||
"})?;
|
||
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg("tqdm")
|
||
.arg("--find-links")
|
||
.arg(context.workspace_root.join("scripts/links/")), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 1 package in [TIME]
|
||
Prepared 1 package in [TIME]
|
||
Installed 1 package in [TIME]
|
||
+ tqdm==1000.0.0
|
||
"###
|
||
);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Sync using `--find-links` with a local directory, with wheels disabled.
|
||
#[test]
|
||
fn find_links_no_binary() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||
requirements_txt.write_str(indoc! {r"
|
||
tqdm
|
||
"})?;
|
||
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg("tqdm")
|
||
.arg("--no-binary")
|
||
.arg(":all:")
|
||
.arg("--find-links")
|
||
.arg(context.workspace_root.join("scripts/links/")), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 1 package in [TIME]
|
||
Prepared 1 package in [TIME]
|
||
Installed 1 package in [TIME]
|
||
+ tqdm==999.0.0
|
||
"###
|
||
);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Provide valid hashes for all dependencies with `--require-hashes`.
|
||
#[test]
|
||
fn require_hashes() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
// Write to a requirements file.
|
||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||
requirements_txt.write_str(indoc::indoc! {r"
|
||
anyio==4.0.0 \
|
||
--hash=sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f \
|
||
--hash=sha256:f7ed51751b2c2add651e5747c891b47e26d2a21be5d32d9311dfe9692f3e5d7a
|
||
idna==3.6 \
|
||
--hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \
|
||
--hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f
|
||
# via anyio
|
||
sniffio==1.3.1 \
|
||
--hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \
|
||
--hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc
|
||
# via anyio
|
||
"})?;
|
||
|
||
uv_snapshot!(context.install()
|
||
.arg("-r")
|
||
.arg("requirements.txt")
|
||
.arg("--require-hashes"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 3 packages in [TIME]
|
||
Prepared 3 packages in [TIME]
|
||
Installed 3 packages in [TIME]
|
||
+ anyio==4.0.0
|
||
+ idna==3.6
|
||
+ sniffio==1.3.1
|
||
"###
|
||
);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Omit hashes for dependencies with `--require-hashes`, which is allowed with `--no-deps`.
|
||
#[test]
|
||
fn require_hashes_no_deps() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
// Write to a requirements file.
|
||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||
requirements_txt.write_str(indoc::indoc! {r"
|
||
anyio==4.0.0 \
|
||
--hash=sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f \
|
||
--hash=sha256:f7ed51751b2c2add651e5747c891b47e26d2a21be5d32d9311dfe9692f3e5d7a
|
||
"})?;
|
||
|
||
uv_snapshot!(context.install()
|
||
.arg("-r")
|
||
.arg("requirements.txt")
|
||
.arg("--no-deps")
|
||
.arg("--require-hashes"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 1 package in [TIME]
|
||
Prepared 1 package in [TIME]
|
||
Installed 1 package in [TIME]
|
||
+ anyio==4.0.0
|
||
"###
|
||
);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Provide the wrong hash with `--require-hashes`.
|
||
#[test]
|
||
fn require_hashes_mismatch() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
// Write to a requirements file.
|
||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||
requirements_txt.write_str(
|
||
"anyio==4.0.0 --hash=sha256:afdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f",
|
||
)?;
|
||
|
||
// Raise an error.
|
||
uv_snapshot!(context.install()
|
||
.arg("-r")
|
||
.arg("requirements.txt")
|
||
.arg("--require-hashes"), @r###"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: In `--require-hashes` mode, all requirements must be pinned upfront with `==`, but found: `idna`
|
||
"###
|
||
);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Omit a transitive dependency in `--require-hashes`.
|
||
#[test]
|
||
fn require_hashes_missing_dependency() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
// Write to a requirements file.
|
||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||
requirements_txt.write_str(
|
||
"anyio==4.0.0 --hash=sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f",
|
||
)?;
|
||
|
||
// Install without error when `--require-hashes` is omitted.
|
||
uv_snapshot!(context.install()
|
||
.arg("-r")
|
||
.arg("requirements.txt")
|
||
.arg("--require-hashes"), @r###"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: In `--require-hashes` mode, all requirements must be pinned upfront with `==`, but found: `idna`
|
||
"###
|
||
);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// We disallow `--require-hashes` for editables' dependencies.
|
||
#[test]
|
||
fn require_hashes_editable() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||
requirements_txt.write_str(&indoc::formatdoc! {r"
|
||
-e file://{workspace_root}/scripts/packages/black_editable[d]
|
||
",
|
||
workspace_root = context.workspace_root.simplified_display(),
|
||
})?;
|
||
|
||
// Install the editable packages.
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg("-r")
|
||
.arg(requirements_txt.path())
|
||
.arg("--require-hashes"), @r###"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: In `--require-hashes` mode, all requirement must have a hash, but none were provided for: file://[WORKSPACE]/scripts/packages/black_editable[d]
|
||
"###
|
||
);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// If a hash is only included as a constraint, that's not good enough for `--require-hashes`.
|
||
#[test]
|
||
fn require_hashes_constraint() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
// Include the hash in the constraint file.
|
||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||
requirements_txt.write_str("anyio==4.0.0")?;
|
||
|
||
let constraints_txt = context.temp_dir.child("constraints.txt");
|
||
constraints_txt.write_str("anyio==4.0.0 --hash=sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f")?;
|
||
|
||
// Install the editable packages.
|
||
uv_snapshot!(context.install()
|
||
.arg("-r")
|
||
.arg(requirements_txt.path())
|
||
.arg("--require-hashes")
|
||
.arg("-c")
|
||
.arg(constraints_txt.path()), @r###"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: In `--require-hashes` mode, all requirement must have a hash, but none were provided for: anyio==4.0.0
|
||
"###
|
||
);
|
||
|
||
// Include the hash in the requirements file, but pin the version in the constraint file.
|
||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||
requirements_txt.write_str(
|
||
"anyio --hash=sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f",
|
||
)?;
|
||
|
||
let constraints_txt = context.temp_dir.child("constraints.txt");
|
||
constraints_txt.write_str("anyio==4.0.0")?;
|
||
|
||
// Install the editable packages.
|
||
uv_snapshot!(context.install()
|
||
.arg("-r")
|
||
.arg(requirements_txt.path())
|
||
.arg("--require-hashes")
|
||
.arg("-c")
|
||
.arg(constraints_txt.path()), @r###"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: In `--require-hashes` mode, all requirement must have their versions pinned with `==`, but found: anyio
|
||
"###
|
||
);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// We allow `--require-hashes` for unnamed URL dependencies.
|
||
#[test]
|
||
fn require_hashes_unnamed() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||
requirements_txt
|
||
.write_str(indoc::indoc! {r"
|
||
https://files.pythonhosted.org/packages/36/55/ad4de788d84a630656ece71059665e01ca793c04294c463fd84132f40fe6/anyio-4.0.0-py3-none-any.whl --hash=sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f
|
||
idna==3.6 \
|
||
--hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \
|
||
--hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f
|
||
# via anyio
|
||
sniffio==1.3.1 \
|
||
--hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \
|
||
--hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc
|
||
# via anyio
|
||
"})?;
|
||
|
||
uv_snapshot!(context.install()
|
||
.arg("-r")
|
||
.arg("requirements.txt")
|
||
.arg("--require-hashes"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 3 packages in [TIME]
|
||
Prepared 3 packages in [TIME]
|
||
Installed 3 packages in [TIME]
|
||
+ anyio==4.0.0 (from https://files.pythonhosted.org/packages/36/55/ad4de788d84a630656ece71059665e01ca793c04294c463fd84132f40fe6/anyio-4.0.0-py3-none-any.whl)
|
||
+ idna==3.6
|
||
+ sniffio==1.3.1
|
||
"###
|
||
);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// We allow `--require-hashes` for unnamed URL dependencies. In this case, the unnamed URL is
|
||
/// a repeat of a registered package.
|
||
#[test]
|
||
fn require_hashes_unnamed_repeated() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
// Re-run, but duplicate `anyio`.
|
||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||
requirements_txt
|
||
.write_str(indoc::indoc! {r"
|
||
anyio==4.0.0 \
|
||
--hash=sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f \
|
||
--hash=sha256:f7ed51751b2c2add651e5747c891b47e26d2a21be5d32d9311dfe9692f3e5d7a
|
||
https://files.pythonhosted.org/packages/36/55/ad4de788d84a630656ece71059665e01ca793c04294c463fd84132f40fe6/anyio-4.0.0-py3-none-any.whl --hash=sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f
|
||
idna==3.6 \
|
||
--hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \
|
||
--hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f
|
||
# via anyio
|
||
sniffio==1.3.1 \
|
||
--hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \
|
||
--hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc
|
||
# via anyio
|
||
"})?;
|
||
|
||
uv_snapshot!(context.install()
|
||
.arg("-r")
|
||
.arg("requirements.txt")
|
||
.arg("--require-hashes"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 3 packages in [TIME]
|
||
Prepared 3 packages in [TIME]
|
||
Installed 3 packages in [TIME]
|
||
+ anyio==4.0.0 (from https://files.pythonhosted.org/packages/36/55/ad4de788d84a630656ece71059665e01ca793c04294c463fd84132f40fe6/anyio-4.0.0-py3-none-any.whl)
|
||
+ idna==3.6
|
||
+ sniffio==1.3.1
|
||
"###
|
||
);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// If a hash is only included as a override, that's not good enough for `--require-hashes`.
|
||
///
|
||
/// TODO(charlie): This _should_ be allowed. It's a bug.
|
||
#[test]
|
||
fn require_hashes_override() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
// Include the hash in the override file.
|
||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||
requirements_txt.write_str("anyio==4.0.0")?;
|
||
|
||
let overrides_txt = context.temp_dir.child("overrides.txt");
|
||
overrides_txt.write_str("anyio==4.0.0 --hash=sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f")?;
|
||
|
||
// Install the editable packages.
|
||
uv_snapshot!(context.install()
|
||
.arg("-r")
|
||
.arg(requirements_txt.path())
|
||
.arg("--require-hashes")
|
||
.arg("--override")
|
||
.arg(overrides_txt.path()), @r###"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: In `--require-hashes` mode, all requirement must have a hash, but none were provided for: anyio==4.0.0
|
||
"###
|
||
);
|
||
|
||
// Include the hash in the requirements file, but pin the version in the override file.
|
||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||
requirements_txt.write_str(
|
||
"anyio --hash=sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f",
|
||
)?;
|
||
|
||
let overrides_txt = context.temp_dir.child("overrides.txt");
|
||
overrides_txt.write_str("anyio==4.0.0")?;
|
||
|
||
// Install the editable packages.
|
||
uv_snapshot!(context.install()
|
||
.arg("-r")
|
||
.arg(requirements_txt.path())
|
||
.arg("--require-hashes")
|
||
.arg("--override")
|
||
.arg(overrides_txt.path()), @r###"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: In `--require-hashes` mode, all requirement must have their versions pinned with `==`, but found: anyio
|
||
"###
|
||
);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn tool_uv_sources() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
// Use a subdir to test path normalization.
|
||
let require_path = "some_dir/pyproject.toml";
|
||
let pyproject_toml = context.temp_dir.child(require_path);
|
||
pyproject_toml.write_str(indoc! {r#"
|
||
[project]
|
||
name = "foo"
|
||
version = "0.0.0"
|
||
dependencies = [
|
||
"tqdm>4,<=5",
|
||
"packaging @ git+https://github.com/pypa/packaging@32deafe8668a2130a3366b98154914d188f3718e",
|
||
"poetry_editable",
|
||
"urllib3 @ https://files.pythonhosted.org/packages/a2/73/a68704750a7679d0b6d3ad7aa8d4da8e14e151ae82e6fee774e6e0d05ec8/urllib3-2.2.1-py3-none-any.whl",
|
||
# Windows consistency
|
||
"colorama>0.4,<5",
|
||
]
|
||
|
||
[project.optional-dependencies]
|
||
utils = [
|
||
"boltons==24.0.0"
|
||
]
|
||
dont_install_me = [
|
||
"broken @ https://example.org/does/not/exist"
|
||
]
|
||
|
||
[tool.uv.sources]
|
||
tqdm = { url = "https://files.pythonhosted.org/packages/a5/d6/502a859bac4ad5e274255576cd3e15ca273cdb91731bc39fb840dd422ee9/tqdm-4.66.0-py3-none-any.whl" }
|
||
boltons = { git = "https://github.com/mahmoud/boltons", rev = "57fbaa9b673ed85b32458b31baeeae230520e4a0" }
|
||
poetry_editable = { path = "../poetry_editable", editable = true }
|
||
"#})?;
|
||
|
||
let project_root = fs_err::canonicalize(std::env::current_dir()?.join("../.."))?;
|
||
fs_err::create_dir_all(context.temp_dir.join("poetry_editable/poetry_editable"))?;
|
||
fs_err::copy(
|
||
project_root.join("scripts/packages/poetry_editable/pyproject.toml"),
|
||
context.temp_dir.join("poetry_editable/pyproject.toml"),
|
||
)?;
|
||
fs_err::copy(
|
||
project_root.join("scripts/packages/poetry_editable/poetry_editable/__init__.py"),
|
||
context
|
||
.temp_dir
|
||
.join("poetry_editable/poetry_editable/__init__.py"),
|
||
)?;
|
||
|
||
// Install the editable packages.
|
||
uv_snapshot!(context.filters(), windows_filters=false, context.install()
|
||
.arg("--preview")
|
||
.arg("-r")
|
||
.arg(require_path)
|
||
.arg("--extra")
|
||
.arg("utils"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 9 packages in [TIME]
|
||
Prepared 9 packages in [TIME]
|
||
Installed 9 packages in [TIME]
|
||
+ anyio==4.3.0
|
||
+ boltons==24.0.1.dev0 (from git+https://github.com/mahmoud/boltons@57fbaa9b673ed85b32458b31baeeae230520e4a0)
|
||
+ colorama==0.4.6
|
||
+ idna==3.6
|
||
+ packaging==24.1.dev0 (from git+https://github.com/pypa/packaging@32deafe8668a2130a3366b98154914d188f3718e)
|
||
+ poetry-editable==0.1.0 (from file://[TEMP_DIR]/poetry_editable)
|
||
+ sniffio==1.3.1
|
||
+ tqdm==4.66.0 (from https://files.pythonhosted.org/packages/a5/d6/502a859bac4ad5e274255576cd3e15ca273cdb91731bc39fb840dd422ee9/tqdm-4.66.0-py3-none-any.whl)
|
||
+ urllib3==2.2.1 (from https://files.pythonhosted.org/packages/a2/73/a68704750a7679d0b6d3ad7aa8d4da8e14e151ae82e6fee774e6e0d05ec8/urllib3-2.2.1-py3-none-any.whl)
|
||
"###
|
||
);
|
||
|
||
// Re-install the editable packages.
|
||
uv_snapshot!(context.filters(), windows_filters=false, context.install()
|
||
.arg("--preview")
|
||
.arg("-r")
|
||
.arg(require_path)
|
||
.arg("--extra")
|
||
.arg("utils"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 9 packages in [TIME]
|
||
Audited 9 packages in [TIME]
|
||
"###
|
||
);
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn tool_uv_sources_is_in_preview() -> 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 = "0.0.0"
|
||
dependencies = [
|
||
"tqdm>4,<=5",
|
||
]
|
||
|
||
[tool.uv.sources]
|
||
tqdm = { url = "https://files.pythonhosted.org/packages/a5/d6/502a859bac4ad5e274255576cd3e15ca273cdb91731bc39fb840dd422ee9/tqdm-4.66.0-py3-none-any.whl" }
|
||
"#})?;
|
||
|
||
// Install the editable packages.
|
||
uv_snapshot!(context.filters(), windows_filters=false, context.install()
|
||
.arg("-r")
|
||
.arg("pyproject.toml")
|
||
.arg("--extra")
|
||
.arg("utils"), @r###"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: Failed to parse entry for: `tqdm`
|
||
Caused by: `tool.uv.sources` is a preview feature; use `--preview` or set `UV_PREVIEW=1` to enable it
|
||
"###
|
||
);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Allow transitive URLs via recursive extras.
|
||
#[test]
|
||
fn recursive_extra_transitive_url() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||
pyproject_toml.write_str(indoc! {r#"
|
||
[project]
|
||
name = "project"
|
||
version = "0.0.0"
|
||
dependencies = []
|
||
|
||
[project.optional-dependencies]
|
||
all = [
|
||
"project[docs]",
|
||
]
|
||
docs = [
|
||
"iniconfig @ https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl",
|
||
]
|
||
"#})?;
|
||
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg(".[all]"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 2 packages in [TIME]
|
||
Prepared 2 packages in [TIME]
|
||
Installed 2 packages in [TIME]
|
||
+ iniconfig==2.0.0 (from https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl)
|
||
+ project==0.0.0 (from file://[TEMP_DIR]/)
|
||
"###);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// If a package is requested as both editable and non-editable, always install it as editable.
|
||
#[test]
|
||
fn prefer_editable() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg("-e")
|
||
.arg(context.workspace_root.join("scripts/packages/black_editable"))
|
||
.arg(context.workspace_root.join("scripts/packages/black_editable")), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 1 package in [TIME]
|
||
Prepared 1 package in [TIME]
|
||
Installed 1 package in [TIME]
|
||
+ black==0.1.0 (from file://[WORKSPACE]/scripts/packages/black_editable)
|
||
"###
|
||
);
|
||
|
||
// Validate that `black.pth` was created.
|
||
let path = context.site_packages().join("black.pth");
|
||
assert!(path.is_file());
|
||
|
||
let context = TestContext::new("3.12");
|
||
|
||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||
requirements_txt.write_str(&format!(
|
||
"black @ file://{}/scripts/packages/black_editable",
|
||
context.workspace_root.simplified_display()
|
||
))?;
|
||
|
||
uv_snapshot!(context.filters(), context.install()
|
||
.arg("-e")
|
||
.arg(context.workspace_root.join("scripts/packages/black_editable"))
|
||
.arg("-r")
|
||
.arg("requirements.txt"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 1 package in [TIME]
|
||
Prepared 1 package in [TIME]
|
||
Installed 1 package in [TIME]
|
||
+ black==0.1.0 (from file://[WORKSPACE]/scripts/packages/black_editable)
|
||
"###
|
||
);
|
||
|
||
// Validate that `black.pth` was created.
|
||
let path = context.site_packages().join("black.pth");
|
||
assert!(path.is_file());
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Resolve against a local directory laid out as a PEP 503-compatible index.
|
||
#[test]
|
||
fn local_index() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
let root = context.temp_dir.child("simple-html");
|
||
fs_err::create_dir_all(&root)?;
|
||
|
||
let tqdm = root.child("tqdm");
|
||
fs_err::create_dir_all(&tqdm)?;
|
||
|
||
let index = tqdm.child("index.html");
|
||
index.write_str(&indoc::formatdoc! {r#"
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<meta name="pypi:repository-version" content="1.1" />
|
||
</head>
|
||
<body>
|
||
<h1>Links for example-a-961b4c22</h1>
|
||
<a
|
||
href="{}/tqdm-1000.0.0-py3-none-any.whl"
|
||
data-requires-python=">=3.8"
|
||
>
|
||
tqdm-1000.0.0-py3-none-any.whl
|
||
</a>
|
||
</body>
|
||
</html>
|
||
"#, Url::from_directory_path(context.workspace_root.join("scripts/links/")).unwrap().as_str()})?;
|
||
|
||
uv_snapshot!(context.filters(), context.install_without_exclude_newer()
|
||
.arg("tqdm")
|
||
.arg("--index-url")
|
||
.arg(Url::from_directory_path(root).unwrap().as_str()), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 1 package in [TIME]
|
||
Prepared 1 package in [TIME]
|
||
Installed 1 package in [TIME]
|
||
+ tqdm==1000.0.0
|
||
"###
|
||
);
|
||
|
||
Ok(())
|
||
}
|