uv/crates/uv/tests/it/run.rs

5556 lines
154 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#![allow(clippy::disallowed_types)]
use anyhow::Result;
use assert_cmd::assert::OutputAssertExt;
use assert_fs::{fixture::ChildPath, prelude::*};
use indoc::indoc;
use insta::assert_snapshot;
use predicates::{prelude::predicate, str::contains};
use std::path::Path;
use uv_fs::copy_dir_all;
use uv_python::PYTHON_VERSION_FILENAME;
use uv_static::EnvVars;
use crate::common::{TestContext, uv_snapshot};
#[test]
fn run_with_python_version() -> Result<()> {
let context = TestContext::new_with_versions(&["3.12", "3.11", "3.9"]);
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(indoc! { r#"
[project]
name = "foo"
version = "1.0.0"
requires-python = ">=3.11, <4"
dependencies = [
"anyio==3.6.0 ; python_version == '3.11'",
"anyio==3.7.0 ; python_version == '3.12'",
]
[build-system]
requires = ["setuptools>=42"]
build-backend = "setuptools.build_meta"
"#
})?;
let test_script = context.temp_dir.child("main.py");
test_script.write_str(indoc! { r#"
import importlib.metadata
import platform
print(platform.python_version())
print(importlib.metadata.version("anyio"))
"#
})?;
// Our tests change files in <1s, so we must disable CPython bytecode caching with `-B` or we'll
// get stale files, see https://github.com/python/cpython/issues/75953.
let mut command = context.run();
let command_with_args = command.arg("python").arg("-B").arg("main.py");
uv_snapshot!(context.filters(), command_with_args, @r###"
success: true
exit_code: 0
----- stdout -----
3.12.[X]
3.7.0
----- stderr -----
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
Creating virtual environment at: .venv
Resolved 5 packages in [TIME]
Prepared 4 packages in [TIME]
Installed 4 packages in [TIME]
+ anyio==3.7.0
+ foo==1.0.0 (from file://[TEMP_DIR]/)
+ idna==3.6
+ sniffio==1.3.1
"###);
// This is the same Python, no reinstallation.
let mut command = context.run();
let command_with_args = command
.arg("-p")
.arg("3.12")
.arg("python")
.arg("-B")
.arg("main.py");
uv_snapshot!(context.filters(), command_with_args, @r###"
success: true
exit_code: 0
----- stdout -----
3.12.[X]
3.7.0
----- stderr -----
Resolved 5 packages in [TIME]
Audited 4 packages in [TIME]
"###);
// This time, we target Python 3.11 instead.
let mut command = context.run();
let command_with_args = command
.arg("-p")
.arg("3.11")
.arg("python")
.arg("-B")
.arg("main.py")
.env_remove(EnvVars::VIRTUAL_ENV);
uv_snapshot!(context.filters(), command_with_args, @r###"
success: true
exit_code: 0
----- stdout -----
3.11.[X]
3.6.0
----- stderr -----
Using CPython 3.11.[X] interpreter at: [PYTHON-3.11]
Removed virtual environment at: .venv
Creating virtual environment at: .venv
Resolved 5 packages in [TIME]
Prepared 1 package in [TIME]
Installed 4 packages in [TIME]
+ anyio==3.6.0
+ foo==1.0.0 (from file://[TEMP_DIR]/)
+ idna==3.6
+ sniffio==1.3.1
"###);
// This time, we target Python 3.9 instead.
let mut command = context.run();
let command_with_args = command
.arg("-p")
.arg("3.9")
.arg("python")
.arg("-B")
.arg("main.py")
.env_remove(EnvVars::VIRTUAL_ENV);
uv_snapshot!(context.filters(), command_with_args, @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
Using CPython 3.9.[X] interpreter at: [PYTHON-3.9]
error: The requested interpreter resolved to Python 3.9.[X], which is incompatible with the project's Python requirement: `>=3.11, <4` (from `project.requires-python`)
");
Ok(())
}
#[test]
fn run_args() -> Result<()> {
let context = TestContext::new("3.12");
let mut filters = context.filters();
filters.push((r"Usage: (uv|\.exe) run \[OPTIONS\] (?s).*", "[UV RUN HELP]"));
filters.push((r"usage: .*(\n|.*)*", "usage: [PYTHON HELP]"));
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(indoc! { r#"
[project]
name = "foo"
version = "1.0.0"
requires-python = ">=3.8"
dependencies = []
[build-system]
requires = ["setuptools>=42"]
build-backend = "setuptools.build_meta"
"#
})?;
// We treat arguments before the command as uv arguments
uv_snapshot!(filters, context.run().arg("--help").arg("python"), @r"
success: true
exit_code: 0
----- stdout -----
Run a command or script
[UV RUN HELP]
");
// We don't treat arguments after the command as uv arguments
uv_snapshot!(filters, context.run().arg("python").arg("--help"), @r"
success: true
exit_code: 0
----- stdout -----
usage: [PYTHON HELP]
");
// Can use `--` to separate uv arguments from the command arguments.
uv_snapshot!(filters, context.run().arg("--").arg("python").arg("--version"), @r###"
success: true
exit_code: 0
----- stdout -----
Python 3.12.[X]
----- stderr -----
Resolved 1 package in [TIME]
Audited 1 package in [TIME]
"###);
Ok(())
}
/// Run without specifying any arguments.
///
/// This should list the available scripts.
#[test]
fn run_no_args() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(indoc! { r#"
[project]
name = "foo"
version = "1.0.0"
requires-python = ">=3.8"
dependencies = []
[build-system]
requires = ["setuptools>=42"]
build-backend = "setuptools.build_meta"
"#
})?;
// Run without specifying any argunments.
#[cfg(not(windows))]
uv_snapshot!(context.filters(), context.run(), @r###"
success: false
exit_code: 2
----- stdout -----
Provide a command or script to invoke with `uv run <command>` or `uv run <script>.py`.
The following commands are available in the environment:
- python
- python3
- python3.12
See `uv run --help` for more information.
----- stderr -----
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ foo==1.0.0 (from file://[TEMP_DIR]/)
"###);
#[cfg(windows)]
uv_snapshot!(context.filters(), context.run(), @r###"
success: false
exit_code: 2
----- stdout -----
Provide a command or script to invoke with `uv run <command>` or `uv run <script>.py`.
The following commands are available in the environment:
- pydoc.bat
- python
- pythonw
See `uv run --help` for more information.
----- stderr -----
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ foo==1.0.0 (from file://[TEMP_DIR]/)
"###);
Ok(())
}
/// Run a PEP 723-compatible script. The script should take precedence over the workspace
/// dependencies.
#[test]
fn run_pep723_script() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(indoc! { r#"
[project]
name = "foo"
version = "1.0.0"
requires-python = ">=3.8"
dependencies = ["anyio"]
[build-system]
requires = ["setuptools>=42"]
build-backend = "setuptools.build_meta"
"#
})?;
// If the script contains a PEP 723 tag, we should install its requirements.
let test_script = context.temp_dir.child("main.py");
test_script.write_str(indoc! { r#"
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "iniconfig",
# ]
# ///
import iniconfig
"#
})?;
// Running the script should install the requirements.
uv_snapshot!(context.filters(), context.run().arg("main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ iniconfig==2.0.0
"###);
// Running again should use the existing environment.
uv_snapshot!(context.filters(), context.run().arg("main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
"###);
// But neither invocation should create a lockfile.
assert!(!context.temp_dir.child("main.py.lock").exists());
// Otherwise, the script requirements should _not_ be available, but the project requirements
// should.
let test_non_script = context.temp_dir.child("main.py");
test_non_script.write_str(indoc! { r"
import iniconfig
"
})?;
uv_snapshot!(context.filters(), context.run().arg("main.py"), @r###"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
Resolved 6 packages in [TIME]
Prepared 4 packages in [TIME]
Installed 4 packages in [TIME]
+ anyio==4.3.0
+ foo==1.0.0 (from file://[TEMP_DIR]/)
+ idna==3.6
+ sniffio==1.3.1
Traceback (most recent call last):
File "[TEMP_DIR]/main.py", line 1, in <module>
import iniconfig
ModuleNotFoundError: No module named 'iniconfig'
"###);
// But the script should be runnable.
let test_non_script = context.temp_dir.child("main.py");
test_non_script.write_str(indoc! { r#"
import idna
print("Hello, world!")
"#
})?;
uv_snapshot!(context.filters(), context.run().arg("main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
Hello, world!
----- stderr -----
Resolved 6 packages in [TIME]
Audited 4 packages in [TIME]
"###);
// If the script contains a PEP 723 tag, it can omit the dependencies field.
let test_script = context.temp_dir.child("main.py");
test_script.write_str(indoc! { r#"
# /// script
# requires-python = ">=3.11"
# ///
print("Hello, world!")
"#
})?;
// Running the script should install the requirements.
uv_snapshot!(context.filters(), context.run().arg("main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
Hello, world!
----- stderr -----
"###);
// Running a script with `--locked` should warn.
uv_snapshot!(context.filters(), context.run().arg("--locked").arg("main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
Hello, world!
----- stderr -----
warning: No lockfile found for Python script (ignoring `--locked`); run `uv lock --script` to generate a lockfile
"###);
// If the script can't be resolved, we should reference the script.
let test_script = context.temp_dir.child("main.py");
test_script.write_str(indoc! { r#"
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "add",
# ]
# ///
"#
})?;
// Running a script with `--group` should warn.
uv_snapshot!(context.filters(), context.run().arg("--group").arg("foo").arg("main.py"), @r###"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
× No solution found when resolving script dependencies:
╰─▶ Because there are no versions of add and you require add, we can conclude that your requirements are unsatisfiable.
"###);
// If the script can't be resolved, we should reference the script.
let test_script = context.temp_dir.child("main.py");
test_script.write_str(indoc! { r#"
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "add",
# ]
# ///
"#
})?;
uv_snapshot!(context.filters(), context.run().arg("--no-project").arg("main.py"), @r###"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
× No solution found when resolving script dependencies:
╰─▶ Because there are no versions of add and you require add, we can conclude that your requirements are unsatisfiable.
"###);
// If the script contains an unclosed PEP 723 tag, we should error.
let test_script = context.temp_dir.child("main.py");
test_script.write_str(indoc! { r#"
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "iniconfig",
# ]
# ///
import iniconfig
"#
})?;
uv_snapshot!(context.filters(), context.run().arg("--no-project").arg("main.py"), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: An opening tag (`# /// script`) was found without a closing tag (`# ///`). Ensure that every line between the opening and closing tags (including empty lines) starts with a leading `#`.
"###);
Ok(())
}
#[test]
fn run_pep723_script_requires_python() -> Result<()> {
let context = TestContext::new_with_versions(&["3.9", "3.11"]);
// If we have a `.python-version` that's incompatible with the script, we should error.
let python_version = context.temp_dir.child(PYTHON_VERSION_FILENAME);
python_version.write_str("3.9")?;
// If the script contains a PEP 723 tag, we should install its requirements.
let test_script = context.temp_dir.child("main.py");
test_script.write_str(indoc! { r#"
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "iniconfig",
# ]
# ///
import iniconfig
x: str | int = "hello"
print(x)
"#
})?;
uv_snapshot!(context.filters(), context.run().arg("main.py"), @r#"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
warning: The Python request from `.python-version` resolved to Python 3.9.[X], which is incompatible with the script's Python requirement: `>=3.11`
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ iniconfig==2.0.0
Traceback (most recent call last):
File "[TEMP_DIR]/main.py", line 10, in <module>
x: str | int = "hello"
TypeError: unsupported operand type(s) for |: 'type' and 'type'
"#);
// Delete the `.python-version` file to allow the script to run.
fs_err::remove_file(&python_version)?;
uv_snapshot!(context.filters(), context.run().arg("main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
hello
----- stderr -----
Resolved 1 package in [TIME]
Installed 1 package in [TIME]
+ iniconfig==2.0.0
"###);
Ok(())
}
/// Run a `.pyw` script. The script should be executed with `pythonw.exe`.
#[test]
fn run_pythonw_script() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(indoc! { r#"
[project]
name = "foo"
version = "1.0.0"
requires-python = ">=3.8"
dependencies = ["anyio"]
[build-system]
requires = ["setuptools>=42"]
build-backend = "setuptools.build_meta"
"#
})?;
let test_script = context.temp_dir.child("main.pyw");
test_script.write_str(indoc! { r"
import anyio
"
})?;
uv_snapshot!(context.filters(), context.run().arg("main.pyw"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 6 packages in [TIME]
Prepared 4 packages in [TIME]
Installed 4 packages in [TIME]
+ anyio==4.3.0
+ foo==1.0.0 (from file://[TEMP_DIR]/)
+ idna==3.6
+ sniffio==1.3.1
"###);
Ok(())
}
/// Run a PEP 723-compatible script with `tool.uv` metadata.
#[test]
#[cfg(feature = "git")]
fn run_pep723_script_metadata() -> Result<()> {
let context = TestContext::new("3.12");
// If the script contains a PEP 723 tag, we should install its requirements.
let test_script = context.temp_dir.child("main.py");
test_script.write_str(indoc! { r#"
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "iniconfig>1",
# ]
#
# [tool.uv]
# resolution = "lowest-direct"
# ///
import iniconfig
"#
})?;
// Running the script should fail without network access.
uv_snapshot!(context.filters(), context.run().arg("main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ iniconfig==1.0.1
"###);
// Respect `tool.uv.sources`.
let test_script = context.temp_dir.child("main.py");
test_script.write_str(indoc! { r#"
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "uv-public-pypackage",
# ]
#
# [tool.uv.sources]
# uv-public-pypackage = { git = "https://github.com/astral-test/uv-public-pypackage", rev = "0dacfd662c64cb4ceb16e6cf65a157a8b715b979" }
# ///
import uv_public_pypackage
"#
})?;
// The script should succeed with the specified source.
uv_snapshot!(context.filters(), context.run().arg("main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ uv-public-pypackage==0.1.0 (from git+https://github.com/astral-test/uv-public-pypackage@0dacfd662c64cb4ceb16e6cf65a157a8b715b979)
"###);
Ok(())
}
/// Run a PEP 723-compatible script with a `[[tool.uv.index]]`.
#[test]
fn run_pep723_script_index() -> Result<()> {
let context = TestContext::new("3.12");
let test_script = context.temp_dir.child("main.py");
test_script.write_str(indoc! { r#"
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "idna>=2",
# ]
#
# [[tool.uv.index]]
# name = "test"
# url = "https://test.pypi.org/simple"
# explicit = true
#
# [tool.uv.sources]
# idna = { index = "test" }
# ///
import idna
"#
})?;
uv_snapshot!(context.filters(), context.run().arg("main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ idna==2.7
"###);
Ok(())
}
/// Run a PEP 723-compatible script with `tool.uv` constraints.
#[test]
fn run_pep723_script_constraints() -> Result<()> {
let context = TestContext::new("3.12");
let test_script = context.temp_dir.child("main.py");
test_script.write_str(indoc! { r#"
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "anyio>=3",
# ]
#
# [tool.uv]
# constraint-dependencies = ["idna<=3"]
# ///
import anyio
"#
})?;
uv_snapshot!(context.filters(), context.run().arg("main.py"), @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.0
+ sniffio==1.3.1
"###);
Ok(())
}
/// Run a PEP 723-compatible script with `tool.uv` overrides.
#[test]
fn run_pep723_script_overrides() -> Result<()> {
let context = TestContext::new("3.12");
let test_script = context.temp_dir.child("main.py");
test_script.write_str(indoc! { r#"
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "anyio>=3",
# ]
#
# [tool.uv]
# override-dependencies = ["idna<=2"]
# ///
import anyio
"#
})?;
uv_snapshot!(context.filters(), context.run().arg("main.py"), @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==2.0
+ sniffio==1.3.1
"###);
Ok(())
}
/// Run a PEP 723-compatible script with `tool.uv` build constraints.
#[test]
fn run_pep723_script_build_constraints() -> Result<()> {
let context = TestContext::new("3.9");
let test_script = context.temp_dir.child("main.py");
// Incompatible build constraints.
test_script.write_str(indoc! { r#"
# /// script
# requires-python = ">=3.9"
# dependencies = [
# "anyio>=3",
# "requests==1.2"
# ]
#
# [tool.uv]
# build-constraint-dependencies = ["setuptools==1"]
# ///
import anyio
"#
})?;
uv_snapshot!(context.filters(), context.run().arg("main.py"), @r###"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
× Failed to download and build `requests==1.2.0`
├─▶ Failed to resolve requirements from `setup.py` build
├─▶ No solution found when resolving: `setuptools>=40.8.0`
╰─▶ Because you require setuptools>=40.8.0 and setuptools==1, we can conclude that your requirements are unsatisfiable.
"###);
// Compatible build constraints.
test_script.write_str(indoc! { r#"
# /// script
# requires-python = ">=3.9"
# dependencies = [
# "anyio>=3",
# "requests==1.2"
# ]
#
# [tool.uv]
# build-constraint-dependencies = ["setuptools>=40"]
# ///
import anyio
"#
})?;
uv_snapshot!(context.filters(), context.run().arg("main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 6 packages in [TIME]
Prepared 6 packages in [TIME]
Installed 6 packages in [TIME]
+ anyio==4.3.0
+ exceptiongroup==1.2.0
+ idna==3.6
+ requests==1.2.0
+ sniffio==1.3.1
+ typing-extensions==4.10.0
"###);
Ok(())
}
/// Run a PEP 723-compatible script with a lockfile.
#[test]
fn run_pep723_script_lock() -> Result<()> {
let context = TestContext::new("3.12");
let test_script = context.temp_dir.child("main.py");
test_script.write_str(indoc! { r#"
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "iniconfig",
# ]
# ///
import iniconfig
print("Hello, world!")
"#
})?;
// Without a lockfile, running with `--locked` should warn.
uv_snapshot!(context.filters(), context.run().arg("--locked").arg("main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
Hello, world!
----- stderr -----
warning: No lockfile found for Python script (ignoring `--locked`); run `uv lock --script` to generate a lockfile
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ iniconfig==2.0.0
"###);
// Explicitly lock the script.
uv_snapshot!(context.filters(), context.lock().arg("--script").arg("main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 1 package in [TIME]
"###);
let lock = context.read("main.py.lock");
insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(
lock, @r#"
version = 1
revision = 2
requires-python = ">=3.11"
[options]
exclude-newer = "2024-03-25T00:00:00Z"
[manifest]
requirements = [{ name = "iniconfig" }]
[[package]]
name = "iniconfig"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646, upload-time = "2023-01-07T11:08:11.254Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892, upload-time = "2023-01-07T11:08:09.864Z" },
]
"#
);
});
// Run the script.
uv_snapshot!(context.filters(), context.run().arg("main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
Hello, world!
----- stderr -----
Resolved 1 package in [TIME]
Audited 1 package in [TIME]
"###);
// With a lockfile, running with `--locked` should not warn.
uv_snapshot!(context.filters(), context.run().arg("--locked").arg("main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
Hello, world!
----- stderr -----
Resolved 1 package in [TIME]
Audited 1 package in [TIME]
"###);
// Modify the metadata.
test_script.write_str(indoc! { r#"
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "anyio",
# ]
# ///
import anyio
print("Hello, world!")
"#
})?;
// Re-running the script with `--locked` should error.
uv_snapshot!(context.filters(), context.run().arg("--locked").arg("main.py"), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
Resolved 3 packages in [TIME]
error: The lockfile at `uv.lock` needs to be updated, but `--locked` was provided. To update the lockfile, run `uv lock`.
"###);
// Re-running the script with `--frozen` should also error, but at runtime.
uv_snapshot!(context.filters(), context.run().arg("--frozen").arg("main.py"), @r###"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
Audited 1 package in [TIME]
Traceback (most recent call last):
File "[TEMP_DIR]/main.py", line 8, in <module>
import anyio
ModuleNotFoundError: No module named 'anyio'
"###);
// Re-running the script should update the lockfile.
uv_snapshot!(context.filters(), context.run().arg("main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
Hello, world!
----- 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 lock = context.read("main.py.lock");
insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(
lock, @r#"
version = 1
revision = 2
requires-python = ">=3.11"
[options]
exclude-newer = "2024-03-25T00:00:00Z"
[manifest]
requirements = [{ name = "anyio" }]
[[package]]
name = "anyio"
version = "4.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
{ name = "sniffio" },
]
sdist = { url = "https://files.pythonhosted.org/packages/db/4d/3970183622f0330d3c23d9b8a5f52e365e50381fd484d08e3285104333d3/anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6", size = 159642, upload-time = "2024-02-19T08:36:28.641Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8", size = 85584, upload-time = "2024-02-19T08:36:26.842Z" },
]
[[package]]
name = "idna"
version = "3.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426, upload-time = "2023-11-25T15:40:54.902Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567, upload-time = "2023-11-25T15:40:52.604Z" },
]
[[package]]
name = "sniffio"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
]
"#
);
});
Ok(())
}
/// With `managed = false`, we should avoid installing the project itself.
#[test]
fn run_managed_false() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(indoc! { r#"
[project]
name = "foo"
version = "1.0.0"
requires-python = ">=3.8"
dependencies = ["anyio"]
[build-system]
requires = ["setuptools>=42"]
build-backend = "setuptools.build_meta"
[tool.uv]
managed = false
"#
})?;
uv_snapshot!(context.filters(), context.run().arg("python").arg("--version"), @r###"
success: true
exit_code: 0
----- stdout -----
Python 3.12.[X]
----- stderr -----
"###);
Ok(())
}
#[test]
fn run_exact() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(indoc! { r#"
[project]
name = "foo"
version = "1.0.0"
requires-python = ">=3.8"
dependencies = ["iniconfig"]
"#
})?;
uv_snapshot!(context.filters(), context.run().arg("python").arg("-c").arg("import iniconfig"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 2 packages in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ iniconfig==2.0.0
"###);
// Remove `iniconfig`.
pyproject_toml.write_str(indoc! { r#"
[project]
name = "foo"
version = "1.0.0"
requires-python = ">=3.8"
dependencies = ["anyio"]
"#
})?;
// By default, `uv run` uses inexact semantics, so both `iniconfig` and `anyio` should still be available.
uv_snapshot!(context.filters(), context.run().arg("python").arg("-c").arg("import iniconfig; import anyio"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 6 packages in [TIME]
Prepared 3 packages in [TIME]
Installed 3 packages in [TIME]
+ anyio==4.3.0
+ idna==3.6
+ sniffio==1.3.1
"###);
// But under `--exact`, `iniconfig` should not be available.
uv_snapshot!(context.filters(), context.run().arg("--exact").arg("python").arg("-c").arg("import iniconfig"), @r###"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
Resolved 6 packages in [TIME]
Uninstalled 1 package in [TIME]
- iniconfig==2.0.0
Traceback (most recent call last):
File "<string>", line 1, in <module>
ModuleNotFoundError: No module named 'iniconfig'
"###);
Ok(())
}
#[test]
fn run_with() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(indoc! { r#"
[project]
name = "foo"
version = "1.0.0"
requires-python = ">=3.8"
dependencies = ["sniffio==1.3.0"]
[build-system]
requires = ["setuptools>=42"]
build-backend = "setuptools.build_meta"
"#
})?;
let test_script = context.temp_dir.child("main.py");
test_script.write_str(indoc! { r"
import sniffio
"
})?;
// Requesting an unsatisfied requirement should install it.
uv_snapshot!(context.filters(), context.run().arg("--with").arg("iniconfig").arg("main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 2 packages in [TIME]
Prepared 2 packages in [TIME]
Installed 2 packages in [TIME]
+ foo==1.0.0 (from file://[TEMP_DIR]/)
+ sniffio==1.3.0
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ iniconfig==2.0.0
"###);
// Requesting a satisfied requirement should use the base environment.
uv_snapshot!(context.filters(), context.run().arg("--with").arg("sniffio").arg("main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 2 packages in [TIME]
Audited 2 packages in [TIME]
"###);
// Unless the user requests a different version.
uv_snapshot!(context.filters(), context.run().arg("--with").arg("sniffio<1.3.0").arg("main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 2 packages in [TIME]
Audited 2 packages in [TIME]
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ sniffio==1.2.0
"###);
// If we request a dependency that isn't in the base environment, we should still respect any
// other dependencies. In this case, `sniffio==1.3.0` is not the latest-compatible version, but
// we should use it anyway.
uv_snapshot!(context.filters(), context.run().arg("--with").arg("anyio").arg("main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 2 packages in [TIME]
Audited 2 packages in [TIME]
Resolved 3 packages in [TIME]
Prepared 2 packages in [TIME]
Installed 3 packages in [TIME]
+ anyio==4.3.0
+ idna==3.6
+ sniffio==1.3.0
"###);
// Even if we run with` --no-sync`.
uv_snapshot!(context.filters(), context.run().arg("--with").arg("anyio==4.2.0").arg("--no-sync").arg("main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 3 packages in [TIME]
Prepared 1 package in [TIME]
Installed 3 packages in [TIME]
+ anyio==4.2.0
+ idna==3.6
+ sniffio==1.3.0
"###);
// If the dependencies can't be resolved, we should reference `--with`.
uv_snapshot!(context.filters(), context.run().arg("--with").arg("add").arg("main.py"), @r###"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
Resolved 2 packages in [TIME]
Audited 2 packages in [TIME]
× No solution found when resolving `--with` dependencies:
╰─▶ Because there are no versions of add and you require add, we can conclude that your requirements are unsatisfiable.
"###);
Ok(())
}
/// Test that an ephemeral environment writes the path of its parent environment to the `extends-environment` key
/// of its `pyvenv.cfg` file. This feature makes it easier for static-analysis tools like ty to resolve which import
/// search paths are available in these ephemeral environments.
#[test]
fn run_with_pyvenv_cfg_file() -> Result<()> {
let context = TestContext::new("3.12").with_pyvenv_cfg_filters();
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(indoc! { r#"
[project]
name = "foo"
version = "1.0.0"
requires-python = ">=3.8"
[build-system]
requires = ["setuptools>=42"]
build-backend = "setuptools.build_meta"
"#
})?;
let test_script = context.temp_dir.child("main.py");
test_script.write_str(indoc! { r#"
import os
with open(f'{os.getenv("VIRTUAL_ENV")}/pyvenv.cfg') as f:
print(f.read())
"#
})?;
uv_snapshot!(context.filters(), context.run().arg("--with").arg("iniconfig").arg("main.py"), @r"
success: true
exit_code: 0
----- stdout -----
home = [PYTHON_HOME]
implementation = CPython
uv = [UV_VERSION]
version_info = 3.12.[X]
include-system-site-packages = false
relocatable = true
extends-environment = [PARENT_VENV]
----- stderr -----
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ foo==1.0.0 (from file://[TEMP_DIR]/)
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ iniconfig==2.0.0
");
Ok(())
}
#[test]
fn run_with_build_constraints() -> Result<()> {
let context = TestContext::new("3.9");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(indoc! { r#"
[project]
name = "foo"
version = "1.0.0"
requires-python = ">=3.9"
dependencies = ["anyio"]
[tool.uv]
build-constraint-dependencies = ["setuptools==1"]
"#
})?;
let test_script = context.temp_dir.child("main.py");
test_script.write_str(indoc! { r"
import os
"
})?;
// Installing requests with incompatible build constraints should fail.
uv_snapshot!(context.filters(), context.run().arg("--with").arg("requests==1.2").arg("main.py"), @r"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
Resolved 6 packages in [TIME]
Prepared 5 packages in [TIME]
Installed 5 packages in [TIME]
+ anyio==4.3.0
+ exceptiongroup==1.2.0
+ idna==3.6
+ sniffio==1.3.1
+ typing-extensions==4.10.0
× Failed to download and build `requests==1.2.0`
├─▶ Failed to resolve requirements from `setup.py` build
├─▶ No solution found when resolving: `setuptools>=40.8.0`
╰─▶ Because you require setuptools>=40.8.0 and setuptools==1, we can conclude that your requirements are unsatisfiable.
");
// Change the build constraint to be compatible with `requests==1.2`.
pyproject_toml.write_str(indoc! { r#"
[project]
name = "foo"
version = "1.0.0"
requires-python = ">=3.9"
dependencies = ["anyio"]
[tool.uv]
build-constraint-dependencies = ["setuptools>=42"]
"#
})?;
uv_snapshot!(context.filters(), context.run().arg("--with").arg("requests==1.2").arg("main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 6 packages in [TIME]
Audited 5 packages in [TIME]
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ requests==1.2.0
"###);
Ok(())
}
/// Sync all members in a workspace.
#[test]
fn run_in_workspace() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["anyio>3"]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.uv.workspace]
members = ["child1", "child2"]
[tool.uv.sources]
child1 = { workspace = true }
child2 = { workspace = true }
"#,
)?;
context
.temp_dir
.child("src")
.child("project")
.child("__init__.py")
.touch()?;
let child1 = context.temp_dir.child("child1");
child1.child("pyproject.toml").write_str(
r#"
[project]
name = "child1"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["iniconfig>1"]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
"#,
)?;
child1
.child("src")
.child("child1")
.child("__init__.py")
.touch()?;
let child2 = context.temp_dir.child("child2");
child2.child("pyproject.toml").write_str(
r#"
[project]
name = "child2"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["typing-extensions>4"]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
"#,
)?;
child2
.child("src")
.child("child2")
.child("__init__.py")
.touch()?;
let test_script = context.temp_dir.child("main.py");
test_script.write_str(indoc! { r"
import anyio
"
})?;
uv_snapshot!(context.filters(), context.run().arg("main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 8 packages in [TIME]
Prepared 4 packages in [TIME]
Installed 4 packages in [TIME]
+ anyio==4.3.0
+ idna==3.6
+ project==0.1.0 (from file://[TEMP_DIR]/)
+ sniffio==1.3.1
"###);
let test_script = context.temp_dir.child("main.py");
test_script.write_str(indoc! { r"
import iniconfig
"
})?;
uv_snapshot!(context.filters(), context.run().arg("main.py"), @r###"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
Resolved 8 packages in [TIME]
Audited 4 packages in [TIME]
Traceback (most recent call last):
File "[TEMP_DIR]/main.py", line 1, in <module>
import iniconfig
ModuleNotFoundError: No module named 'iniconfig'
"###);
uv_snapshot!(context.filters(), context.run().arg("--package").arg("child1").arg("main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 8 packages in [TIME]
Prepared 2 packages in [TIME]
Installed 2 packages in [TIME]
+ child1==0.1.0 (from file://[TEMP_DIR]/child1)
+ iniconfig==2.0.0
"###);
let test_script = context.temp_dir.child("main.py");
test_script.write_str(indoc! { r"
import typing_extensions
"
})?;
uv_snapshot!(context.filters(), context.run().arg("main.py"), @r###"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
Resolved 8 packages in [TIME]
Audited 4 packages in [TIME]
Traceback (most recent call last):
File "[TEMP_DIR]/main.py", line 1, in <module>
import typing_extensions
ModuleNotFoundError: No module named 'typing_extensions'
"###);
uv_snapshot!(context.filters(), context.run().arg("--all-packages").arg("main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 8 packages in [TIME]
Prepared 2 packages in [TIME]
Installed 2 packages in [TIME]
+ child2==0.1.0 (from file://[TEMP_DIR]/child2)
+ typing-extensions==4.10.0
"###);
Ok(())
}
#[test]
fn run_with_editable() -> Result<()> {
let context = TestContext::new("3.12");
let anyio_local = context.temp_dir.child("src").child("anyio_local");
copy_dir_all(
context.workspace_root.join("scripts/packages/anyio_local"),
&anyio_local,
)?;
let black_editable = context.temp_dir.child("src").child("black_editable");
copy_dir_all(
context
.workspace_root
.join("scripts/packages/black_editable"),
&black_editable,
)?;
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(indoc! { r#"
[project]
name = "foo"
version = "1.0.0"
requires-python = ">=3.8"
dependencies = ["anyio", "sniffio==1.3.1"]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
"#
})?;
context
.temp_dir
.child("src")
.child("foo")
.child("__init__.py")
.touch()?;
let test_script = context.temp_dir.child("main.py");
test_script.write_str(indoc! { r"
import sniffio
"
})?;
// Requesting an editable requirement should install it in a layer.
uv_snapshot!(context.filters(), context.run().arg("--with-editable").arg("./src/black_editable").arg("main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 6 packages in [TIME]
Prepared 4 packages in [TIME]
Installed 4 packages in [TIME]
+ anyio==4.3.0
+ foo==1.0.0 (from file://[TEMP_DIR]/)
+ idna==3.6
+ sniffio==1.3.1
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ black==0.1.0 (from file://[TEMP_DIR]/src/black_editable)
"###);
// Requesting an editable requirement should install it in a layer, even if it satisfied
uv_snapshot!(context.filters(), context.run().arg("--with-editable").arg("./src/anyio_local").arg("main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 6 packages in [TIME]
Audited 4 packages in [TIME]
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ anyio==4.3.0+foo (from file://[TEMP_DIR]/src/anyio_local)
"###);
// Requesting the project itself should use the base environment.
uv_snapshot!(context.filters(), context.run().arg("--with-editable").arg(".").arg("main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 6 packages in [TIME]
Audited 4 packages in [TIME]
"###);
// Similarly, an already editable requirement does not require a layer
pyproject_toml.write_str(indoc! { r#"
[project]
name = "foo"
version = "1.0.0"
requires-python = ">=3.8"
dependencies = ["anyio", "sniffio==1.3.1"]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.uv.sources]
anyio = { path = "./src/anyio_local", editable = true }
"#
})?;
uv_snapshot!(context.filters(), context.sync(), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 3 packages in [TIME]
Prepared 1 package in [TIME]
Uninstalled 3 packages in [TIME]
Installed 2 packages in [TIME]
- anyio==4.3.0
+ anyio==4.3.0+foo (from file://[TEMP_DIR]/src/anyio_local)
~ foo==1.0.0 (from file://[TEMP_DIR]/)
- idna==3.6
"###);
uv_snapshot!(context.filters(), context.run().arg("--with-editable").arg("./src/anyio_local").arg("main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 3 packages in [TIME]
Audited 3 packages in [TIME]
"###);
// If invalid, we should reference `--with-editable`.
uv_snapshot!(context.filters(), context.run().arg("--with-editable").arg("./foo").arg("main.py"), @r###"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
Resolved 3 packages in [TIME]
Audited 3 packages in [TIME]
× Failed to resolve `--with` requirement
╰─▶ Distribution not found at: file://[TEMP_DIR]/foo
"###);
Ok(())
}
#[test]
fn run_group() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["typing-extensions"]
[dependency-groups]
foo = ["anyio"]
bar = ["iniconfig"]
dev = ["sniffio"]
"#,
)?;
let test_script = context.temp_dir.child("main.py");
test_script.write_str(indoc! { r#"
try:
import anyio
print("imported `anyio`")
except ImportError:
print("failed to import `anyio`")
try:
import iniconfig
print("imported `iniconfig`")
except ImportError:
print("failed to import `iniconfig`")
try:
import typing_extensions
print("imported `typing_extensions`")
except ImportError:
print("failed to import `typing_extensions`")
"#
})?;
context.lock().assert().success();
uv_snapshot!(context.filters(), context.run().arg("main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
failed to import `anyio`
failed to import `iniconfig`
imported `typing_extensions`
----- stderr -----
Resolved 6 packages in [TIME]
Prepared 2 packages in [TIME]
Installed 2 packages in [TIME]
+ sniffio==1.3.1
+ typing-extensions==4.10.0
"###);
uv_snapshot!(context.filters(), context.run().arg("--only-group").arg("bar").arg("main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
failed to import `anyio`
imported `iniconfig`
imported `typing_extensions`
----- stderr -----
Resolved 6 packages in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ iniconfig==2.0.0
"###);
uv_snapshot!(context.filters(), context.run().arg("--group").arg("foo").arg("main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
imported `anyio`
imported `iniconfig`
imported `typing_extensions`
----- stderr -----
Resolved 6 packages in [TIME]
Prepared 2 packages in [TIME]
Installed 2 packages in [TIME]
+ anyio==4.3.0
+ idna==3.6
"###);
uv_snapshot!(context.filters(), context.run().arg("--group").arg("foo").arg("--group").arg("bar").arg("main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
imported `anyio`
imported `iniconfig`
imported `typing_extensions`
----- stderr -----
Resolved 6 packages in [TIME]
Audited 5 packages in [TIME]
"###);
uv_snapshot!(context.filters(), context.run().arg("--all-groups").arg("main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
imported `anyio`
imported `iniconfig`
imported `typing_extensions`
----- stderr -----
Resolved 6 packages in [TIME]
Audited 5 packages in [TIME]
"###);
uv_snapshot!(context.filters(), context.run().arg("--all-groups").arg("--no-group").arg("bar").arg("main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
imported `anyio`
imported `iniconfig`
imported `typing_extensions`
----- stderr -----
Resolved 6 packages in [TIME]
Audited 4 packages in [TIME]
"###);
uv_snapshot!(context.filters(), context.run().arg("--group").arg("foo").arg("--no-project").arg("main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
imported `anyio`
imported `iniconfig`
imported `typing_extensions`
----- stderr -----
warning: `--group foo` has no effect when used alongside `--no-project`
"###);
uv_snapshot!(context.filters(), context.run().arg("--group").arg("foo").arg("--group").arg("bar").arg("--no-project").arg("main.py"), @r"
success: true
exit_code: 0
----- stdout -----
imported `anyio`
imported `iniconfig`
imported `typing_extensions`
----- stderr -----
warning: `--group` has no effect when used alongside `--no-project`
");
uv_snapshot!(context.filters(), context.run().arg("--group").arg("dev").arg("--no-project").arg("main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
imported `anyio`
imported `iniconfig`
imported `typing_extensions`
----- stderr -----
warning: `--group dev` has no effect when used alongside `--no-project`
"###);
uv_snapshot!(context.filters(), context.run().arg("--all-groups").arg("--no-project").arg("main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
imported `anyio`
imported `iniconfig`
imported `typing_extensions`
----- stderr -----
warning: `--all-groups` has no effect when used alongside `--no-project`
"###);
uv_snapshot!(context.filters(), context.run().arg("--dev").arg("--no-project").arg("main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
imported `anyio`
imported `iniconfig`
imported `typing_extensions`
----- stderr -----
warning: `--dev` has no effect when used alongside `--no-project`
"###);
Ok(())
}
#[test]
fn run_locked() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["anyio==3.7.0"]
[build-system]
requires = ["setuptools>=42"]
build-backend = "setuptools.build_meta"
"#,
)?;
// Running with `--locked` should error, if no lockfile is present.
uv_snapshot!(context.filters(), context.run().arg("--locked").arg("--").arg("python").arg("--version"), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Unable to find lockfile at `uv.lock`. To create a lockfile, run `uv lock` or `uv sync`.
"###);
// Lock the initial requirements.
context.lock().assert().success();
let existing = context.read("uv.lock");
insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(
existing, @r#"
version = 1
revision = 2
requires-python = ">=3.12"
[options]
exclude-newer = "2024-03-25T00:00:00Z"
[[package]]
name = "anyio"
version = "3.7.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
{ name = "sniffio" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c6/b3/fefbf7e78ab3b805dec67d698dc18dd505af7a18a8dd08868c9b4fa736b5/anyio-3.7.0.tar.gz", hash = "sha256:275d9973793619a5374e1c89a4f4ad3f4b0a5510a2b5b939444bee8f4c4d37ce", size = 142737, upload-time = "2023-05-27T11:12:46.688Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/68/fe/7ce1926952c8a403b35029e194555558514b365ad77d75125f521a2bec62/anyio-3.7.0-py3-none-any.whl", hash = "sha256:eddca883c4175f14df8aedce21054bfca3adb70ffe76a9f607aef9d7fa2ea7f0", size = 80873, upload-time = "2023-05-27T11:12:44.474Z" },
]
[[package]]
name = "idna"
version = "3.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426, upload-time = "2023-11-25T15:40:54.902Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567, upload-time = "2023-11-25T15:40:52.604Z" },
]
[[package]]
name = "project"
version = "0.1.0"
source = { editable = "." }
dependencies = [
{ name = "anyio" },
]
[package.metadata]
requires-dist = [{ name = "anyio", specifier = "==3.7.0" }]
[[package]]
name = "sniffio"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
]
"#);
}
);
// Update the requirements.
pyproject_toml.write_str(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["iniconfig"]
[build-system]
requires = ["setuptools>=42"]
build-backend = "setuptools.build_meta"
"#,
)?;
// Running with `--locked` should error.
uv_snapshot!(context.filters(), context.run().arg("--locked").arg("--").arg("python").arg("--version"), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
Resolved 2 packages in [TIME]
error: The lockfile at `uv.lock` needs to be updated, but `--locked` was provided. To update the lockfile, run `uv lock`.
"###);
let updated = context.read("uv.lock");
// And the lockfile should be unchanged.
assert_eq!(existing, updated);
// Lock the updated requirements.
uv_snapshot!(context.lock(), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 2 packages in [TIME]
Removed anyio v3.7.0
Removed idna v3.6
Added iniconfig v2.0.0
Removed sniffio v1.3.1
"###);
// Lock the updated requirements.
uv_snapshot!(context.lock(), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 2 packages in [TIME]
"###);
// Running with `--locked` should succeed.
uv_snapshot!(context.filters(), context.run().arg("--locked").arg("--").arg("python").arg("--version"), @r###"
success: true
exit_code: 0
----- stdout -----
Python 3.12.[X]
----- stderr -----
Resolved 2 packages in [TIME]
Prepared 2 packages in [TIME]
Installed 2 packages in [TIME]
+ iniconfig==2.0.0
+ project==0.1.0 (from file://[TEMP_DIR]/)
"###);
Ok(())
}
#[test]
fn run_frozen() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["anyio==3.7.0"]
[build-system]
requires = ["setuptools>=42"]
build-backend = "setuptools.build_meta"
"#,
)?;
// Running with `--frozen` should error, if no lockfile is present.
uv_snapshot!(context.filters(), context.run().arg("--frozen").arg("--").arg("python").arg("--version"), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Unable to find lockfile at `uv.lock`. To create a lockfile, run `uv lock` or `uv sync`.
"###);
context.lock().assert().success();
// Update the requirements.
pyproject_toml.write_str(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["iniconfig"]
[build-system]
requires = ["setuptools>=42"]
build-backend = "setuptools.build_meta"
"#,
)?;
// Running with `--frozen` should install the stale lockfile.
uv_snapshot!(context.filters(), context.run().arg("--frozen").arg("--").arg("python").arg("--version"), @r###"
success: true
exit_code: 0
----- stdout -----
Python 3.12.[X]
----- stderr -----
Prepared 4 packages in [TIME]
Installed 4 packages in [TIME]
+ anyio==3.7.0
+ idna==3.6
+ project==0.1.0 (from file://[TEMP_DIR]/)
+ sniffio==1.3.1
"###);
Ok(())
}
#[test]
fn run_no_sync() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["anyio==3.7.0"]
[build-system]
requires = ["setuptools>=42"]
build-backend = "setuptools.build_meta"
"#,
)?;
// Running with `--no-sync` should succeed error, even if the lockfile isn't present.
uv_snapshot!(context.filters(), context.run().arg("--no-sync").arg("--").arg("python").arg("--version"), @r###"
success: true
exit_code: 0
----- stdout -----
Python 3.12.[X]
----- stderr -----
"###);
context.lock().assert().success();
// Running with `--no-sync` should not install any requirements.
uv_snapshot!(context.filters(), context.run().arg("--no-sync").arg("--").arg("python").arg("--version"), @r###"
success: true
exit_code: 0
----- stdout -----
Python 3.12.[X]
----- stderr -----
"###);
context.sync().assert().success();
// But it should have access to the installed packages.
uv_snapshot!(context.filters(), context.run().arg("--no-sync").arg("--").arg("python").arg("-c").arg("import anyio; print(anyio.__name__)"), @r###"
success: true
exit_code: 0
----- stdout -----
anyio
----- stderr -----
"###);
Ok(())
}
#[test]
fn run_empty_requirements_txt() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(indoc! { r#"
[project]
name = "foo"
version = "1.0.0"
requires-python = ">=3.8"
dependencies = ["anyio", "sniffio==1.3.1"]
[build-system]
requires = ["setuptools>=42"]
build-backend = "setuptools.build_meta"
"#
})?;
let test_script = context.temp_dir.child("main.py");
test_script.write_str(indoc! { r"
import sniffio
"
})?;
let requirements_txt =
ChildPath::new(context.temp_dir.canonicalize()?.join("requirements.txt"));
requirements_txt.touch()?;
// The project environment is synced on the first invocation.
uv_snapshot!(context.filters(), context.run().arg("--with-requirements").arg(requirements_txt.as_os_str()).arg("main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 6 packages in [TIME]
Prepared 4 packages in [TIME]
Installed 4 packages in [TIME]
+ anyio==4.3.0
+ foo==1.0.0 (from file://[TEMP_DIR]/)
+ idna==3.6
+ sniffio==1.3.1
warning: Requirements file `requirements.txt` does not contain any dependencies
"###);
// Then reused in subsequent invocations
uv_snapshot!(context.filters(), context.run().arg("--with-requirements").arg(requirements_txt.as_os_str()).arg("main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 6 packages in [TIME]
Audited 4 packages in [TIME]
warning: Requirements file `requirements.txt` does not contain any dependencies
"###);
Ok(())
}
#[test]
fn run_requirements_txt() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(indoc! { r#"
[project]
name = "foo"
version = "1.0.0"
requires-python = ">=3.8"
dependencies = ["anyio", "sniffio==1.3.1"]
[build-system]
requires = ["setuptools>=42"]
build-backend = "setuptools.build_meta"
"#
})?;
let test_script = context.temp_dir.child("main.py");
test_script.write_str(indoc! { r"
import sniffio
"
})?;
// Requesting an unsatisfied requirement should install it.
let requirements_txt = context.temp_dir.child("requirements.txt");
requirements_txt.write_str("iniconfig")?;
uv_snapshot!(context.filters(), context.run().arg("--with-requirements").arg(requirements_txt.as_os_str()).arg("main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 6 packages in [TIME]
Prepared 4 packages in [TIME]
Installed 4 packages in [TIME]
+ anyio==4.3.0
+ foo==1.0.0 (from file://[TEMP_DIR]/)
+ idna==3.6
+ sniffio==1.3.1
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ iniconfig==2.0.0
"###);
// Requesting a satisfied requirement should use the base environment.
requirements_txt.write_str("sniffio")?;
uv_snapshot!(context.filters(), context.run().arg("--with-requirements").arg(requirements_txt.as_os_str()).arg("main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 6 packages in [TIME]
Audited 4 packages in [TIME]
"###);
// Unless the user requests a different version.
requirements_txt.write_str("sniffio<1.3.1")?;
uv_snapshot!(context.filters(), context.run().arg("--with-requirements").arg(requirements_txt.as_os_str()).arg("main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 6 packages in [TIME]
Audited 4 packages in [TIME]
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ sniffio==1.3.0
"###);
// Or includes an unsatisfied requirement via `--with`.
requirements_txt.write_str("sniffio")?;
uv_snapshot!(context.filters(), context.run()
.arg("--with-requirements")
.arg(requirements_txt.as_os_str())
.arg("--with")
.arg("iniconfig")
.arg("main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 6 packages in [TIME]
Audited 4 packages in [TIME]
Resolved 2 packages in [TIME]
Installed 2 packages in [TIME]
+ iniconfig==2.0.0
+ sniffio==1.3.1
"###);
// Allow `-` for stdin.
uv_snapshot!(context.filters(), context.run()
.arg("--with-requirements")
.arg("-")
.arg("--with")
.arg("iniconfig")
.arg("main.py")
.stdin(std::fs::File::open(&requirements_txt)?), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 6 packages in [TIME]
Audited 4 packages in [TIME]
Resolved 2 packages in [TIME]
"###);
// But not in combination with reading the script from stdin
uv_snapshot!(context.filters(), context.run()
.arg("--with-requirements")
.arg("-")
// The script to run
.arg("-")
.stdin(std::fs::File::open(&requirements_txt)?), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Cannot read both requirements file and script from stdin
"###);
uv_snapshot!(context.filters(), context.run()
.arg("--with-requirements")
.arg("-")
.arg("--script")
.arg("-")
.stdin(std::fs::File::open(&requirements_txt)?), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Cannot read both requirements file and script from stdin
"###);
Ok(())
}
/// Ignore and warn when (e.g.) the `--index-url` argument is a provided `requirements.txt`.
#[test]
fn run_requirements_txt_arguments() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(indoc! { r#"
[project]
name = "foo"
version = "1.0.0"
requires-python = ">=3.8"
dependencies = ["typing_extensions"]
[build-system]
requires = ["setuptools>=42"]
build-backend = "setuptools.build_meta"
"#
})?;
let test_script = context.temp_dir.child("main.py");
test_script.write_str(indoc! { r"
import typing_extensions
"
})?;
// Requesting an unsatisfied requirement should install it.
let requirements_txt = context.temp_dir.child("requirements.txt");
requirements_txt.write_str(indoc! { r"
--index-url https://test.pypi.org/simple
idna
"
})?;
uv_snapshot!(context.filters(), context.run().arg("--with-requirements").arg(requirements_txt.as_os_str()).arg("main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 2 packages in [TIME]
Prepared 2 packages in [TIME]
Installed 2 packages in [TIME]
+ foo==1.0.0 (from file://[TEMP_DIR]/)
+ typing-extensions==4.10.0
warning: Ignoring `--index-url` from requirements file: `https://test.pypi.org/simple`. Instead, use the `--index-url` command-line argument, or set `index-url` in a `uv.toml` or `pyproject.toml` file.
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ idna==3.6
"###);
Ok(())
}
/// Ensure that we can import from the root project when layering `--with` requirements.
#[test]
fn run_editable() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(indoc! { r#"
[project]
name = "foo"
version = "1.0.0"
requires-python = ">=3.8"
dependencies = []
[build-system]
requires = ["setuptools>=42"]
build-backend = "setuptools.build_meta"
"#
})?;
let src = context.temp_dir.child("src").child("foo");
src.create_dir_all()?;
let init = src.child("__init__.py");
init.touch()?;
let main = context.temp_dir.child("main.py");
main.write_str(indoc! { r"
import foo
print('Hello, world!')
"
})?;
// We treat arguments before the command as uv arguments
uv_snapshot!(context.filters(), context.run().arg("--with").arg("iniconfig").arg("main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
Hello, world!
----- stderr -----
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ foo==1.0.0 (from file://[TEMP_DIR]/)
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ iniconfig==2.0.0
"###);
Ok(())
}
#[test]
fn run_from_directory() -> Result<()> {
// Default to 3.11 so that the `.python-version` is meaningful.
let context = TestContext::new_with_versions(&["3.10", "3.11", "3.12"])
.with_filtered_missing_file_error();
let project_dir = context.temp_dir.child("project");
project_dir
.child(PYTHON_VERSION_FILENAME)
.write_str("3.12")?;
let pyproject_toml = project_dir.child("pyproject.toml");
pyproject_toml.write_str(indoc! { r#"
[project]
name = "foo"
version = "1.0.0"
requires-python = ">=3.10"
dependencies = []
[project.scripts]
main = "main:main"
[build-system]
requires = ["setuptools>=42"]
build-backend = "setuptools.build_meta"
"#
})?;
let main_script = project_dir.child("main.py");
main_script.write_str(indoc! { r"
import platform
def main():
print(platform.python_version())
"
})?;
let filters = TestContext::path_patterns(Path::new("project").join(".venv"))
.into_iter()
.map(|pattern| (pattern, "[PROJECT_VENV]/".to_string()))
.collect::<Vec<_>>();
let filters = context
.filters()
.into_iter()
.chain(
filters
.iter()
.map(|(pattern, replacement)| (pattern.as_str(), replacement.as_str())),
)
.collect::<Vec<_>>();
// Use `--project`, which resolves configuration relative to the provided directory, but paths
// relative to the current working directory.
uv_snapshot!(filters.clone(), context.run().arg("--project").arg("project").arg("main"), @r###"
success: true
exit_code: 0
----- stdout -----
3.12.[X]
----- stderr -----
warning: `VIRTUAL_ENV=.venv` does not match the project environment path `[PROJECT_VENV]/` and will be ignored; use `--active` to target the active environment instead
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
Creating virtual environment at: [PROJECT_VENV]/
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ foo==1.0.0 (from file://[TEMP_DIR]/project)
"###);
fs_err::remove_dir_all(context.temp_dir.join("project").join(".venv"))?;
uv_snapshot!(filters.clone(), context.run().arg("--project").arg("project").arg("./project/main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
warning: `VIRTUAL_ENV=.venv` does not match the project environment path `[PROJECT_VENV]/` and will be ignored; use `--active` to target the active environment instead
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
Creating virtual environment at: [PROJECT_VENV]/
Resolved 1 package in [TIME]
Installed 1 package in [TIME]
+ foo==1.0.0 (from file://[TEMP_DIR]/project)
"###);
// Use `--directory`, which switches to the provided directory entirely.
fs_err::remove_dir_all(context.temp_dir.join("project").join(".venv"))?;
uv_snapshot!(filters.clone(), context.run().arg("--directory").arg("project").arg("main"), @r###"
success: true
exit_code: 0
----- stdout -----
3.12.[X]
----- stderr -----
warning: `VIRTUAL_ENV=[VENV]/` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
Creating virtual environment at: .venv
Resolved 1 package in [TIME]
Installed 1 package in [TIME]
+ foo==1.0.0 (from file://[TEMP_DIR]/project)
"###);
fs_err::remove_dir_all(context.temp_dir.join("project").join(".venv"))?;
uv_snapshot!(filters.clone(), context.run().arg("--directory").arg("project").arg("./main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
warning: `VIRTUAL_ENV=[VENV]/` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
Creating virtual environment at: .venv
Resolved 1 package in [TIME]
Installed 1 package in [TIME]
+ foo==1.0.0 (from file://[TEMP_DIR]/project)
"###);
fs_err::remove_dir_all(context.temp_dir.join("project").join(".venv"))?;
uv_snapshot!(filters.clone(), context.run().arg("--directory").arg("project").arg("./project/main.py"), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
warning: `VIRTUAL_ENV=[VENV]/` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
Creating virtual environment at: .venv
Resolved 1 package in [TIME]
Installed 1 package in [TIME]
+ foo==1.0.0 (from file://[TEMP_DIR]/project)
error: Failed to spawn: `./project/main.py`
Caused by: [OS ERROR 2]
"###);
// Even if we write a `.python-version` file in the current directory, we should prefer the
// one in the project directory in both cases.
context
.temp_dir
.child(PYTHON_VERSION_FILENAME)
.write_str("3.11")?;
project_dir
.child(PYTHON_VERSION_FILENAME)
.write_str("3.10")?;
fs_err::remove_dir_all(context.temp_dir.join("project").join(".venv"))?;
uv_snapshot!(filters.clone(), context.run().arg("--project").arg("project").arg("main"), @r###"
success: true
exit_code: 0
----- stdout -----
3.10.[X]
----- stderr -----
warning: `VIRTUAL_ENV=.venv` does not match the project environment path `[PROJECT_VENV]/` and will be ignored; use `--active` to target the active environment instead
Using CPython 3.10.[X] interpreter at: [PYTHON-3.10]
Creating virtual environment at: [PROJECT_VENV]/
Resolved 1 package in [TIME]
Installed 1 package in [TIME]
+ foo==1.0.0 (from file://[TEMP_DIR]/project)
"###);
fs_err::remove_dir_all(context.temp_dir.join("project").join(".venv"))?;
uv_snapshot!(filters.clone(), context.run().arg("--directory").arg("project").arg("main"), @r###"
success: true
exit_code: 0
----- stdout -----
3.10.[X]
----- stderr -----
warning: `VIRTUAL_ENV=[VENV]/` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead
Using CPython 3.10.[X] interpreter at: [PYTHON-3.10]
Creating virtual environment at: .venv
Resolved 1 package in [TIME]
Installed 1 package in [TIME]
+ foo==1.0.0 (from file://[TEMP_DIR]/project)
"###);
Ok(())
}
/// By default, omit resolver and installer output.
#[test]
fn run_without_output() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(indoc! { r#"
[project]
name = "foo"
version = "1.0.0"
requires-python = ">=3.8"
dependencies = ["anyio", "sniffio==1.3.1"]
[build-system]
requires = ["setuptools>=42"]
build-backend = "setuptools.build_meta"
"#
})?;
let test_script = context.temp_dir.child("main.py");
test_script.write_str(indoc! { r"
import sniffio
"
})?;
// On the first run, we only show the summary line for each environment.
uv_snapshot!(context.filters(), context.run().env_remove(EnvVars::UV_SHOW_RESOLUTION).arg("--with").arg("iniconfig").arg("main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Installed 4 packages in [TIME]
Installed 1 package in [TIME]
"###);
// Subsequent runs are quiet.
uv_snapshot!(context.filters(), context.run().env_remove(EnvVars::UV_SHOW_RESOLUTION).arg("--with").arg("iniconfig").arg("main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
"###);
Ok(())
}
/// Ensure that we can import from the root project when layering `--with` requirements.
#[test]
fn run_isolated_python_version() -> Result<()> {
let context = TestContext::new_with_versions(&["3.9", "3.12"]);
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(indoc! { r#"
[project]
name = "foo"
version = "1.0.0"
requires-python = ">=3.8"
dependencies = ["anyio"]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
"#
})?;
let src = context.temp_dir.child("src").child("foo");
src.create_dir_all()?;
let init = src.child("__init__.py");
init.touch()?;
let main = context.temp_dir.child("main.py");
main.write_str(indoc! { r"
import sys
print((sys.version_info.major, sys.version_info.minor))
"
})?;
uv_snapshot!(context.filters(), context.run().arg("main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
(3, 9)
----- stderr -----
Using CPython 3.9.[X] interpreter at: [PYTHON-3.9]
Creating virtual environment at: .venv
Resolved 6 packages in [TIME]
Prepared 6 packages in [TIME]
Installed 6 packages in [TIME]
+ anyio==4.3.0
+ exceptiongroup==1.2.0
+ foo==1.0.0 (from file://[TEMP_DIR]/)
+ idna==3.6
+ sniffio==1.3.1
+ typing-extensions==4.10.0
"###);
uv_snapshot!(context.filters(), context.run().arg("--isolated").arg("main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
(3, 9)
----- stderr -----
Resolved 6 packages in [TIME]
Installed 6 packages in [TIME]
+ anyio==4.3.0
+ exceptiongroup==1.2.0
+ foo==1.0.0 (from file://[TEMP_DIR]/)
+ idna==3.6
+ sniffio==1.3.1
+ typing-extensions==4.10.0
"###);
// Set the `.python-version` to `3.12`.
context
.temp_dir
.child(PYTHON_VERSION_FILENAME)
.write_str("3.12")?;
uv_snapshot!(context.filters(), context.run().arg("--isolated").arg("main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
(3, 12)
----- stderr -----
Resolved 6 packages in [TIME]
Installed 4 packages in [TIME]
+ anyio==4.3.0
+ foo==1.0.0 (from file://[TEMP_DIR]/)
+ idna==3.6
+ sniffio==1.3.1
"###);
Ok(())
}
/// Ignore the existing project when executing with `--no-project`.
#[test]
fn run_no_project() -> Result<()> {
let context = TestContext::new("3.12")
.with_filtered_python_names()
.with_filtered_virtualenv_bin()
.with_filtered_exe_suffix();
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(indoc! { r#"
[project]
name = "foo"
version = "1.0.0"
requires-python = ">=3.8"
dependencies = ["anyio"]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
"#
})?;
let src = context.temp_dir.child("src").child("foo");
src.create_dir_all()?;
let init = src.child("__init__.py");
init.touch()?;
// `run` should run in the context of the project.
uv_snapshot!(context.filters(), context.run().arg("python").arg("-c").arg("import sys; print(sys.executable)"), @r"
success: true
exit_code: 0
----- stdout -----
[VENV]/[BIN]/[PYTHON]
----- stderr -----
Resolved 6 packages in [TIME]
Prepared 4 packages in [TIME]
Installed 4 packages in [TIME]
+ anyio==4.3.0
+ foo==1.0.0 (from file://[TEMP_DIR]/)
+ idna==3.6
+ sniffio==1.3.1
");
// `run --no-project` should not (but it should still run in the same environment, as it would
// if there were no project at all).
uv_snapshot!(context.filters(), context.run().arg("--no-project").arg("python").arg("-c").arg("import sys; print(sys.executable)"), @r"
success: true
exit_code: 0
----- stdout -----
[VENV]/[BIN]/[PYTHON]
----- stderr -----
");
// `run --no-project --isolated` should run in an entirely isolated environment.
uv_snapshot!(context.filters(), context.run().arg("--no-project").arg("--isolated").arg("python").arg("-c").arg("import sys; print(sys.executable)"), @r"
success: true
exit_code: 0
----- stdout -----
[CACHE_DIR]/builds-v0/[TMP]/[PYTHON]
----- stderr -----
");
// `run --no-project` should not (but it should still run in the same environment, as it would
// if there were no project at all).
uv_snapshot!(context.filters(), context.run().arg("--no-project").arg("python").arg("-c").arg("import sys; print(sys.executable)"), @r"
success: true
exit_code: 0
----- stdout -----
[VENV]/[BIN]/[PYTHON]
----- stderr -----
");
// `run --no-project --locked` should fail.
uv_snapshot!(context.filters(), context.run().arg("--no-project").arg("--locked").arg("python").arg("-c").arg("import sys; print(sys.executable)"), @r"
success: true
exit_code: 0
----- stdout -----
[VENV]/[BIN]/[PYTHON]
----- stderr -----
warning: `--locked` has no effect when used alongside `--no-project`
");
Ok(())
}
#[test]
fn run_stdin() -> Result<()> {
let context = TestContext::new("3.12");
let test_script = context.temp_dir.child("main.py");
test_script.write_str(indoc! { r#"
print("Hello, world!")
"#
})?;
let mut command = context.run();
let command_with_args = command.stdin(std::fs::File::open(test_script)?).arg("-");
uv_snapshot!(context.filters(), command_with_args, @r###"
success: true
exit_code: 0
----- stdout -----
Hello, world!
----- stderr -----
"###);
Ok(())
}
#[test]
fn run_package() -> Result<()> {
let context = TestContext::new("3.12");
let main_script = context.temp_dir.child("__main__.py");
main_script.write_str(indoc! { r#"
print("Hello, world!")
"#
})?;
uv_snapshot!(context.filters(), context.run().arg("."), @r###"
success: true
exit_code: 0
----- stdout -----
Hello, world!
----- stderr -----
"###);
Ok(())
}
#[test]
fn run_zipapp() -> Result<()> {
let context = TestContext::new("3.12");
// Create a zipapp.
let child = context.temp_dir.child("app");
child.create_dir_all()?;
let main_script = child.child("__main__.py");
main_script.write_str(indoc! { r#"
print("Hello, world!")
"#
})?;
let zipapp = context.temp_dir.child("app.pyz");
let status = context
.run()
.arg("python")
.arg("-m")
.arg("zipapp")
.arg(child.as_ref())
.arg("--output")
.arg(zipapp.as_ref())
.status()?;
assert!(status.success());
// Run the zipapp.
uv_snapshot!(context.filters(), context.run().arg(zipapp.as_ref()), @r###"
success: true
exit_code: 0
----- stdout -----
Hello, world!
----- stderr -----
"###);
Ok(())
}
#[test]
fn run_stdin_args() {
let context = TestContext::new("3.12");
uv_snapshot!(context.filters(), context.run().arg("python").arg("-c").arg("import sys; print(sys.argv)").arg("foo").arg("bar"), @r###"
success: true
exit_code: 0
----- stdout -----
['-c', 'foo', 'bar']
----- stderr -----
"###);
}
/// Run a module equivalent to `python -m foo`.
#[test]
fn run_module() {
let context = TestContext::new("3.12");
uv_snapshot!(context.filters(), context.run().arg("-m").arg("__hello__"), @r#"
success: true
exit_code: 0
----- stdout -----
Hello world!
----- stderr -----
"#);
uv_snapshot!(context.filters(), context.run().arg("-m").arg("http.server").arg("-h"), @r#"
success: true
exit_code: 0
----- stdout -----
usage: server.py [-h] [--cgi] [-b ADDRESS] [-d DIRECTORY] [-p VERSION] [port]
positional arguments:
port bind to this port (default: 8000)
options:
-h, --help show this help message and exit
--cgi run as CGI server
-b ADDRESS, --bind ADDRESS
bind to this address (default: all interfaces)
-d DIRECTORY, --directory DIRECTORY
serve this directory (default: current directory)
-p VERSION, --protocol VERSION
conform to this HTTP version (default: HTTP/1.0)
----- stderr -----
"#);
}
#[test]
fn run_module_stdin() {
let context = TestContext::new("3.12");
uv_snapshot!(context.filters(), context.run().arg("-m").arg("-"), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Cannot run a Python module from stdin
"###);
}
/// When the `pyproject.toml` file is invalid.
#[test]
fn run_project_toml_error() -> Result<()> {
let context = TestContext::new("3.12")
.with_filtered_python_names()
.with_filtered_virtualenv_bin()
.with_filtered_exe_suffix();
// Create an empty project
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.touch()?;
let src = context.temp_dir.child("src").child("foo");
src.create_dir_all()?;
let init = src.child("__init__.py");
init.touch()?;
// `run` should fail
uv_snapshot!(context.filters(), context.run().arg("python").arg("-c").arg("import sys; print(sys.executable)"), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: No `project` table found in: `[TEMP_DIR]/pyproject.toml`
"###);
// `run --no-project` should not
uv_snapshot!(context.filters(), context.run().arg("--no-project").arg("python").arg("-c").arg("import sys; print(sys.executable)"), @r"
success: true
exit_code: 0
----- stdout -----
[VENV]/[BIN]/[PYTHON]
----- stderr -----
");
Ok(())
}
#[test]
fn run_isolated_incompatible_python() -> Result<()> {
let context = TestContext::new_with_versions(&["3.9", "3.11"]);
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(indoc! { r#"
[project]
name = "foo"
version = "1.0.0"
requires-python = ">=3.12"
dependencies = ["iniconfig"]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
"#
})?;
let python_version = context.temp_dir.child(PYTHON_VERSION_FILENAME);
python_version.write_str("3.9")?;
let test_script = context.temp_dir.child("main.py");
test_script.write_str(indoc! { r#"
import iniconfig
x: str | int = "hello"
print(x)
"#
})?;
// We should reject Python 3.9...
uv_snapshot!(context.filters(), context.run().arg("main.py"), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
Using CPython 3.9.[X] interpreter at: [PYTHON-3.9]
error: The Python request from `.python-version` resolved to Python 3.9.[X], which is incompatible with the project's Python requirement: `>=3.12` (from `project.requires-python`)
Use `uv python pin` to update the `.python-version` file to a compatible version
");
// ...even if `--isolated` is provided.
uv_snapshot!(context.filters(), context.run().arg("--isolated").arg("main.py"), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: The Python request from `.python-version` resolved to Python 3.9.[X], which is incompatible with the project's Python requirement: `>=3.12` (from `project.requires-python`)
Use `uv python pin` to update the `.python-version` file to a compatible version
");
Ok(())
}
#[test]
fn run_compiled_python_file() -> Result<()> {
let context = TestContext::new("3.12");
// Write a non-PEP 723 script.
let test_non_script = context.temp_dir.child("main.py");
test_non_script.write_str(indoc! { r#"
print("Hello, world!")
"#
})?;
// Run a non-PEP 723 script.
uv_snapshot!(context.filters(), context.run().arg("main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
Hello, world!
----- stderr -----
"###);
let compile_output = context
.run()
.arg("python")
.arg("-m")
.arg("compileall")
.arg(test_non_script.path())
.output()?;
assert!(
compile_output.status.success(),
"Failed to compile the python script"
);
// Run the compiled non-PEP 723 script.
let compiled_non_script = context.temp_dir.child("__pycache__/main.cpython-312.pyc");
uv_snapshot!(context.filters(), context.run().arg(compiled_non_script.path()), @r###"
success: true
exit_code: 0
----- stdout -----
Hello, world!
----- stderr -----
"###);
// If the script contains a PEP 723 tag, we should install its requirements.
let test_script = context.temp_dir.child("script.py");
test_script.write_str(indoc! { r#"
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "iniconfig",
# ]
# ///
import iniconfig
"#
})?;
uv_snapshot!(context.filters(), context.run().arg("script.py"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ iniconfig==2.0.0
"###);
// Compile the PEP 723 script.
let compile_output = context
.run()
.arg("python")
.arg("-m")
.arg("compileall")
.arg(test_script.path())
.output()?;
assert!(
compile_output.status.success(),
"Failed to compile the python script"
);
// Run the compiled PEP 723 script. This fails, since we can't read the script tag.
let compiled_script = context.temp_dir.child("__pycache__/script.cpython-312.pyc");
uv_snapshot!(context.filters(), context.run().arg(compiled_script.path()), @r###"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
Traceback (most recent call last):
File "[TEMP_DIR]/script.py", line 7, in <module>
import iniconfig
ModuleNotFoundError: No module named 'iniconfig'
"###);
Ok(())
}
#[test]
fn run_exit_code() -> Result<()> {
let context = TestContext::new("3.12");
let test_script = context.temp_dir.child("script.py");
test_script.write_str(indoc! { r#"
# /// script
# requires-python = ">=3.11"
# ///
exit(42)
"#
})?;
context.run().arg("script.py").assert().code(42);
Ok(())
}
#[test]
fn run_invalid_project_table() -> Result<()> {
let context = TestContext::new_with_versions(&["3.12"]);
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(indoc! { r#"
[project.urls]
repository = 'https://github.com/octocat/octocat-python'
[build-system]
requires = ["setuptools>=42"]
build-backend = "setuptools.build_meta"
"#
})?;
let test_script = context.temp_dir.child("main.py");
test_script.write_str(indoc! { r#"
print("Hello, world!")
"#
})?;
uv_snapshot!(context.filters(), context.run().arg("main.py"), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Failed to parse: `pyproject.toml`
Caused by: TOML parse error at line 1, column 2
|
1 | [project.urls]
| ^^^^^^^
`pyproject.toml` is using the `[project]` table, but the required `project.name` field is not set
"###);
Ok(())
}
#[test]
#[cfg(target_family = "unix")]
fn run_script_without_build_system() -> 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.1.0"
requires-python = ">=3.12"
dependencies = []
[project.scripts]
entry = "foo:custom_entry"
"#
})?;
let test_script = context.temp_dir.child("src/__init__.py");
test_script.write_str(indoc! { r#"
def custom_entry():
print!("Hello")
"#
})?;
// TODO(lucab): this should match `entry` and warn
// <https://github.com/astral-sh/uv/issues/7428>
uv_snapshot!(context.filters(), context.run().arg("entry"), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
Resolved 1 package in [TIME]
Audited in [TIME]
error: Failed to spawn: `entry`
Caused by: No such file or directory (os error 2)
"###);
Ok(())
}
#[test]
fn run_script_module_conflict() -> 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.1.0"
requires-python = ">=3.12"
dependencies = []
[project.scripts]
foo = "foo:app"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
"#
})?;
let init = context.temp_dir.child("src/foo/__init__.py");
init.write_str(indoc! { r#"
def app():
print("Hello from `__init__`")
"#
})?;
uv_snapshot!(context.filters(), context.run().arg("foo"), @r###"
success: true
exit_code: 0
----- stdout -----
Hello from `__init__`
----- stderr -----
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ foo==0.1.0 (from file://[TEMP_DIR]/)
"###);
// Creating `__main__` should not change the behavior, the entrypoint should take precedence
let main = context.temp_dir.child("src/foo/__main__.py");
main.write_str(indoc! { r#"
print("Hello from `__main__`")
"#
})?;
uv_snapshot!(context.filters(), context.run().arg("foo"), @r###"
success: true
exit_code: 0
----- stdout -----
Hello from `__init__`
----- stderr -----
Resolved 1 package in [TIME]
Audited 1 package in [TIME]
"###);
// Even if the working directory is `src`
uv_snapshot!(context.filters(), context.run().arg("--directory").arg("src").arg("foo"), @r###"
success: true
exit_code: 0
----- stdout -----
Hello from `__init__`
----- stderr -----
Resolved 1 package in [TIME]
Audited 1 package in [TIME]
"###);
// Unless the user opts-in to module running with `-m`
uv_snapshot!(context.filters(), context.run().arg("-m").arg("foo"), @r###"
success: true
exit_code: 0
----- stdout -----
Hello from `__main__`
----- stderr -----
Resolved 1 package in [TIME]
Audited 1 package in [TIME]
"###);
Ok(())
}
#[test]
fn run_script_explicit() -> Result<()> {
let context = TestContext::new("3.12");
let test_script = context.temp_dir.child("script");
test_script.write_str(indoc! { r#"
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "iniconfig",
# ]
# ///
import iniconfig
print("Hello, world!")
"#
})?;
uv_snapshot!(context.filters(), context.run().arg("--script").arg("script"), @r###"
success: true
exit_code: 0
----- stdout -----
Hello, world!
----- stderr -----
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ iniconfig==2.0.0
"###);
Ok(())
}
#[test]
fn run_script_explicit_stdin() -> Result<()> {
let context = TestContext::new("3.12");
let test_script = context.temp_dir.child("script");
test_script.write_str(indoc! { r#"
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "iniconfig",
# ]
# ///
import iniconfig
print("Hello, world!")
"#
})?;
uv_snapshot!(context.filters(), context.run().arg("--script").arg("-").stdin(std::fs::File::open(test_script)?), @r###"
success: true
exit_code: 0
----- stdout -----
Hello, world!
----- stderr -----
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ iniconfig==2.0.0
"###);
Ok(())
}
#[test]
fn run_script_explicit_no_file() {
let context = TestContext::new("3.12");
context
.run()
.arg("--script")
.arg("script")
.assert()
.stderr(contains("can't open file"))
.stderr(contains("[Errno 2] No such file or directory"));
}
#[cfg(target_family = "unix")]
#[test]
fn run_script_explicit_directory() -> Result<()> {
let context = TestContext::new("3.12");
fs_err::create_dir(context.temp_dir.child("script"))?;
uv_snapshot!(context.filters(), context.run().arg("--script").arg("script"), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: failed to read from file `script`: Is a directory (os error 21)
"###);
Ok(())
}
#[test]
#[cfg(windows)]
fn run_gui_script_explicit_windows() -> Result<()> {
let context = TestContext::new("3.12");
let test_script = context.temp_dir.child("script");
test_script.write_str(indoc! { r#"
# /// script
# requires-python = ">=3.11"
# dependencies = []
# ///
import sys
import os
executable = os.path.basename(sys.executable).lower()
if not executable.startswith("pythonw"):
print(f"Error: Expected pythonw.exe but got: {executable}", file=sys.stderr)
sys.exit(1)
print(f"Using executable: {executable}", file=sys.stderr)
"#})?;
uv_snapshot!(context.filters(), context.run().arg("--gui-script").arg("script"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using executable: pythonw.exe
"###);
Ok(())
}
#[test]
#[cfg(windows)]
fn run_gui_script_explicit_stdin_windows() -> Result<()> {
let context = TestContext::new("3.12");
let test_script = context.temp_dir.child("script");
test_script.write_str(indoc! { r#"
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "iniconfig",
# ]
# ///
import iniconfig
print("Hello, world!")
"#
})?;
uv_snapshot!(context.filters(), context.run().arg("--gui-script").arg("-").stdin(std::fs::File::open(test_script)?), @r###"
success: true
exit_code: 0
----- stdout -----
Hello, world!
----- stderr -----
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ iniconfig==2.0.0
"###);
Ok(())
}
#[test]
#[cfg(not(windows))]
fn run_gui_script_explicit_unix() -> Result<()> {
let context = TestContext::new("3.12");
let test_script = context.temp_dir.child("script");
test_script.write_str(indoc! { r#"
# /// script
# requires-python = ">=3.11"
# dependencies = []
# ///
import sys
import os
executable = os.path.basename(sys.executable).lower()
print(f"Using executable: {executable}", file=sys.stderr)
"#})?;
uv_snapshot!(context.filters(), context.run().arg("--gui-script").arg("script"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using executable: python
"###);
Ok(())
}
#[test]
#[cfg(unix)]
fn run_linked_environment_path() -> Result<()> {
use anyhow::Ok;
let context = TestContext::new("3.12")
.with_filtered_virtualenv_bin()
.with_filtered_python_names();
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["black"]
"#,
)?;
// Create a link from `target` -> virtual environment
fs_err::os::unix::fs::symlink(&context.venv, context.temp_dir.child("target"))?;
// Running `uv sync` should use the environment at `target``
uv_snapshot!(context.filters(), context.sync()
.env(EnvVars::UV_PROJECT_ENVIRONMENT, "target"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 8 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
"###);
// `sys.prefix` and `sys.executable` should be from the `target` directory
uv_snapshot!(context.filters(), context.run()
.env_remove("VIRTUAL_ENV") // Ignore the test context's active virtual environment
.env(EnvVars::UV_PROJECT_ENVIRONMENT, "target")
.arg("python").arg("-c").arg("import sys; print(sys.prefix); print(sys.executable)"), @r"
success: true
exit_code: 0
----- stdout -----
[TEMP_DIR]/target
[TEMP_DIR]/target/[BIN]/[PYTHON]
----- stderr -----
Resolved 8 packages in [TIME]
Audited 6 packages in [TIME]
");
// And, similarly, the entrypoint should use `target`
let black_entrypoint = context.read("target/bin/black");
insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(
black_entrypoint, @r##"
#![TEMP_DIR]/target/[BIN]/[PYTHON]
# -*- coding: utf-8 -*-
import sys
from black import patched_main
if __name__ == "__main__":
if sys.argv[0].endswith("-script.pyw"):
sys.argv[0] = sys.argv[0][:-11]
elif sys.argv[0].endswith(".exe"):
sys.argv[0] = sys.argv[0][:-4]
sys.exit(patched_main())
"##
);
});
Ok(())
}
#[test]
fn run_active_project_environment() -> Result<()> {
let context = TestContext::new_with_versions(&["3.11", "3.12"])
.with_filtered_virtualenv_bin()
.with_filtered_python_names();
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = ["iniconfig"]
"#,
)?;
// Running `uv run` with `VIRTUAL_ENV` should warn
uv_snapshot!(context.filters(), context.run()
.arg("python").arg("--version")
.env(EnvVars::VIRTUAL_ENV, "foo"), @r###"
success: true
exit_code: 0
----- stdout -----
Python 3.11.[X]
----- stderr -----
warning: `VIRTUAL_ENV=foo` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead
Using CPython 3.11.[X] interpreter at: [PYTHON-3.11]
Creating virtual environment at: .venv
Resolved 2 packages in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ iniconfig==2.0.0
"###);
// Using `--no-active` should silence the warning
uv_snapshot!(context.filters(), context.run()
.arg("--no-active")
.arg("python").arg("--version")
.env(EnvVars::VIRTUAL_ENV, "foo"), @r###"
success: true
exit_code: 0
----- stdout -----
Python 3.11.[X]
----- stderr -----
Resolved 2 packages in [TIME]
Audited 1 package in [TIME]
"###);
context
.temp_dir
.child(".venv")
.assert(predicate::path::is_dir());
context
.temp_dir
.child("foo")
.assert(predicate::path::missing());
// Using `--active` should create the environment
uv_snapshot!(context.filters(), context.run()
.arg("--active")
.arg("python").arg("--version")
.env(EnvVars::VIRTUAL_ENV, "foo"), @r###"
success: true
exit_code: 0
----- stdout -----
Python 3.11.[X]
----- stderr -----
Using CPython 3.11.[X] interpreter at: [PYTHON-3.11]
Creating virtual environment at: foo
Resolved 2 packages in [TIME]
Installed 1 package in [TIME]
+ iniconfig==2.0.0
"###);
context
.temp_dir
.child("foo")
.assert(predicate::path::is_dir());
// Requesting a different Python version should invalidate the environment
uv_snapshot!(context.filters(), context.run()
.arg("--active")
.arg("-p").arg("3.12")
.arg("python").arg("--version")
.env(EnvVars::VIRTUAL_ENV, "foo"), @r###"
success: true
exit_code: 0
----- stdout -----
Python 3.12.[X]
----- stderr -----
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
Removed virtual environment at: foo
Creating virtual environment at: foo
Resolved 2 packages in [TIME]
Installed 1 package in [TIME]
+ iniconfig==2.0.0
"###);
Ok(())
}
#[test]
fn run_active_script_environment() -> Result<()> {
let context = TestContext::new_with_versions(&["3.11", "3.12"])
.with_filtered_virtualenv_bin()
.with_filtered_python_names();
let test_script = context.temp_dir.child("main.py");
test_script.write_str(indoc! { r#"
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "iniconfig",
# ]
# ///
import iniconfig
print("Hello, world!")
"#
})?;
let filters = context
.filters()
.into_iter()
.chain(vec![(
r"environments-v1/main-\w+",
"environments-v1/main-[HASH]",
)])
.collect::<Vec<_>>();
// Running `uv run --script` with `VIRTUAL_ENV` should _not_ warn.
uv_snapshot!(&filters, context.run()
.arg("--script")
.arg("main.py")
.env(EnvVars::VIRTUAL_ENV, "foo"), @r###"
success: true
exit_code: 0
----- stdout -----
Hello, world!
----- stderr -----
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ iniconfig==2.0.0
"###);
// Using `--no-active` should also _not_ warn.
uv_snapshot!(&filters, context.run()
.arg("--no-active")
.arg("--script")
.arg("main.py")
.env(EnvVars::VIRTUAL_ENV, "foo"), @r###"
success: true
exit_code: 0
----- stdout -----
Hello, world!
----- stderr -----
"###);
context
.temp_dir
.child("foo")
.assert(predicate::path::missing());
// Using `--active` should create the environment
uv_snapshot!(&filters, context.run()
.arg("--active")
.arg("--script")
.arg("main.py")
.env(EnvVars::VIRTUAL_ENV, "foo"), @r###"
success: true
exit_code: 0
----- stdout -----
Hello, world!
----- stderr -----
Resolved 1 package in [TIME]
Installed 1 package in [TIME]
+ iniconfig==2.0.0
"###);
context
.temp_dir
.child("foo")
.assert(predicate::path::is_dir());
// Requesting a different Python version should invalidate the environment
uv_snapshot!(&filters, context.run()
.arg("--active")
.arg("-p").arg("3.12")
.arg("--script")
.arg("main.py")
.env(EnvVars::VIRTUAL_ENV, "foo"), @r###"
success: true
exit_code: 0
----- stdout -----
Hello, world!
----- stderr -----
Resolved 1 package in [TIME]
Installed 1 package in [TIME]
+ iniconfig==2.0.0
"###);
Ok(())
}
#[test]
#[cfg(not(windows))]
fn run_gui_script_explicit_stdin_unix() -> Result<()> {
let context = TestContext::new("3.12");
let test_script = context.temp_dir.child("script");
test_script.write_str(indoc! { r#"
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "iniconfig",
# ]
# ///
import iniconfig
print("Hello, world!")
"#
})?;
uv_snapshot!(context.filters(), context.run().arg("--gui-script").arg("-").stdin(std::fs::File::open(test_script)?), @r###"
success: true
exit_code: 0
----- stdout -----
Hello, world!
----- stderr -----
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ iniconfig==2.0.0
"###);
Ok(())
}
#[test]
fn run_remote_pep723_script() {
let context = TestContext::new("3.12").with_filtered_python_names();
let mut filters = context.filters();
filters.push((
r"(?m)^Downloaded remote script to:.*\.py$",
"Downloaded remote script to: [TEMP_PATH].py",
));
uv_snapshot!(filters, context.run().arg("https://raw.githubusercontent.com/astral-sh/uv/df45b9ac2584824309ff29a6a09421055ad730f6/scripts/uv-run-remote-script-test.py").arg("CI"), @r###"
success: true
exit_code: 0
----- stdout -----
Hello CI, from uv!
----- stderr -----
Resolved 4 packages in [TIME]
Prepared 4 packages in [TIME]
Installed 4 packages in [TIME]
+ markdown-it-py==3.0.0
+ mdurl==0.1.2
+ pygments==2.17.2
+ rich==13.7.1
"###);
}
#[cfg(unix)] // A URL could be a valid filepath on Unix but not on Windows
#[test]
fn run_url_like_with_local_file_priority() -> Result<()> {
let context = TestContext::new("3.12");
let url = "https://example.com/path/to/main.py";
let local_path: std::path::PathBuf = ["https:", "", "example.com", "path", "to", "main.py"]
.iter()
.collect();
// replace with URL-like filepath
let test_script = context.temp_dir.child(local_path);
test_script.write_str(indoc! { r#"
print("Hello, world!")
"#
})?;
uv_snapshot!(context.filters(), context.run().arg(url), @r###"
success: true
exit_code: 0
----- stdout -----
Hello, world!
----- stderr -----
"###);
Ok(())
}
#[test]
fn run_stdin_with_pep723() -> Result<()> {
let context = TestContext::new("3.12");
let test_script = context.temp_dir.child("main.py");
test_script.write_str(indoc! { r#"
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "iniconfig",
# ]
# ///
import iniconfig
print("Hello, world!")
"#
})?;
uv_snapshot!(context.filters(), context.run().stdin(std::fs::File::open(test_script)?).arg("-"), @r###"
success: true
exit_code: 0
----- stdout -----
Hello, world!
----- stderr -----
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ iniconfig==2.0.0
"###);
Ok(())
}
#[test]
fn run_with_env() -> Result<()> {
let context = TestContext::new("3.12");
context.temp_dir.child("test.py").write_str(indoc! { "
import os
print(os.environ.get('THE_EMPIRE_VARIABLE'))
print(os.environ.get('REBEL_1'))
print(os.environ.get('REBEL_2'))
print(os.environ.get('REBEL_3'))
"
})?;
context.temp_dir.child(".env").write_str(indoc! { "
THE_EMPIRE_VARIABLE=palpatine
REBEL_1=leia_organa
REBEL_2=obi_wan_kenobi
REBEL_3=C3PO
"
})?;
uv_snapshot!(context.filters(), context.run().arg("test.py"), @r###"
success: true
exit_code: 0
----- stdout -----
None
None
None
None
----- stderr -----
"###);
uv_snapshot!(context.filters(), context.run().arg("--env-file").arg(".env").arg("test.py"), @r###"
success: true
exit_code: 0
----- stdout -----
palpatine
leia_organa
obi_wan_kenobi
C3PO
----- stderr -----
"###);
Ok(())
}
#[test]
fn run_with_env_file() -> Result<()> {
let context = TestContext::new("3.12");
context.temp_dir.child("test.py").write_str(indoc! { "
import os
print(os.environ.get('THE_EMPIRE_VARIABLE'))
print(os.environ.get('REBEL_1'))
print(os.environ.get('REBEL_2'))
print(os.environ.get('REBEL_3'))
"
})?;
context.temp_dir.child(".file").write_str(indoc! { "
THE_EMPIRE_VARIABLE=palpatine
REBEL_1=leia_organa
REBEL_2=obi_wan_kenobi
REBEL_3=C3PO
"
})?;
uv_snapshot!(context.filters(), context.run().arg("--env-file").arg(".file").arg("test.py"), @r###"
success: true
exit_code: 0
----- stdout -----
palpatine
leia_organa
obi_wan_kenobi
C3PO
----- stderr -----
"###);
Ok(())
}
#[test]
fn run_with_multiple_env_files() -> Result<()> {
let context = TestContext::new("3.12");
context.temp_dir.child("test.py").write_str(indoc! { "
import os
print(os.environ.get('THE_EMPIRE_VARIABLE'))
print(os.environ.get('REBEL_1'))
print(os.environ.get('REBEL_2'))
"
})?;
context.temp_dir.child(".env1").write_str(indoc! { "
THE_EMPIRE_VARIABLE=palpatine
REBEL_1=leia_organa
"
})?;
context.temp_dir.child(".env2").write_str(indoc! { "
THE_EMPIRE_VARIABLE=palpatine
REBEL_1=obi_wan_kenobi
REBEL_2=C3PO
"
})?;
uv_snapshot!(context.filters(), context.run().arg("--env-file").arg(".env1").arg("--env-file").arg(".env2").arg("test.py"), @r###"
success: true
exit_code: 0
----- stdout -----
palpatine
obi_wan_kenobi
C3PO
----- stderr -----
"###);
uv_snapshot!(context.filters(), context.run().arg("test.py").env(EnvVars::UV_ENV_FILE, ".env1 .env2"), @r###"
success: true
exit_code: 0
----- stdout -----
palpatine
obi_wan_kenobi
C3PO
----- stderr -----
"###);
Ok(())
}
#[test]
fn run_with_env_omitted() -> Result<()> {
let context = TestContext::new("3.12");
context.temp_dir.child("test.py").write_str(indoc! { "
import os
print(os.environ.get('THE_EMPIRE_VARIABLE'))
"
})?;
context.temp_dir.child(".env").write_str(indoc! { "
THE_EMPIRE_VARIABLE=palpatine
"
})?;
uv_snapshot!(context.filters(), context.run().arg("--env-file").arg(".env").arg("--no-env-file").arg("test.py"), @r###"
success: true
exit_code: 0
----- stdout -----
None
----- stderr -----
"###);
Ok(())
}
#[test]
fn run_with_malformed_env() -> Result<()> {
let context = TestContext::new("3.12");
context.temp_dir.child("test.py").write_str(indoc! { "
import os
print(os.environ.get('THE_EMPIRE_VARIABLE'))
"
})?;
context.temp_dir.child(".env").write_str(indoc! { "
THE_^EMPIRE_VARIABLE=darth_vader
"
})?;
uv_snapshot!(context.filters(), context.run().arg("--env-file").arg(".env").arg("test.py"), @r###"
success: true
exit_code: 0
----- stdout -----
None
----- stderr -----
warning: Failed to parse environment file `.env` at position 4: THE_^EMPIRE_VARIABLE=darth_vader
"###);
Ok(())
}
#[test]
fn run_with_not_existing_env_file() -> Result<()> {
let context = TestContext::new("3.12");
context.temp_dir.child("test.py").write_str(indoc! { "
import os
print(os.environ.get('THE_EMPIRE_VARIABLE'))
"
})?;
let mut filters = context.filters();
filters.push((
r"(?m)^error: Failed to read environment file `.env.development`: .*$",
"error: Failed to read environment file `.env.development`: [ERR]",
));
uv_snapshot!(filters, context.run().arg("--env-file").arg(".env.development").arg("test.py"), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: No environment file found at: `.env.development`
"###);
Ok(())
}
#[test]
fn run_with_extra_conflict() -> 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.1.0"
requires-python = ">=3.12.0"
dependencies = []
[project.optional-dependencies]
foo = ["iniconfig==2.0.0"]
bar = ["iniconfig==1.1.1"]
[tool.uv]
conflicts = [
[
{ extra = "foo" },
{ extra = "bar" },
],
]
"#
})?;
uv_snapshot!(context.filters(), context.run()
.arg("--extra")
.arg("foo")
.arg("python")
.arg("-c")
.arg("import iniconfig"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 3 packages in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ iniconfig==2.0.0
"###);
Ok(())
}
#[test]
fn run_with_group_conflict() -> 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.1.0"
requires-python = ">=3.12.0"
dependencies = []
[dependency-groups]
foo = ["iniconfig==2.0.0"]
bar = ["iniconfig==1.1.1"]
[tool.uv]
conflicts = [
[
{ group = "foo" },
{ group = "bar" },
],
]
"#
})?;
uv_snapshot!(context.filters(), context.run()
.arg("--group")
.arg("foo")
.arg("python")
.arg("-c")
.arg("import iniconfig"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 3 packages in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ iniconfig==2.0.0
"###);
Ok(())
}
#[test]
fn run_default_groups() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["typing-extensions"]
[dependency-groups]
foo = ["anyio"]
bar = ["iniconfig"]
dev = ["sniffio"]
"#,
)?;
context.lock().assert().success();
// Only the main dependencies and `dev` group should be installed.
uv_snapshot!(context.filters(), context.run().arg("python").arg("-c").arg("import typing_extensions"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 6 packages in [TIME]
Prepared 2 packages in [TIME]
Installed 2 packages in [TIME]
+ sniffio==1.3.1
+ typing-extensions==4.10.0
");
// If we set a different default group, it should be synced instead.
pyproject_toml.write_str(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["typing-extensions"]
[dependency-groups]
foo = ["anyio"]
bar = ["iniconfig"]
dev = ["sniffio"]
[tool.uv]
default-groups = ["foo"]
"#,
)?;
uv_snapshot!(context.filters(), context.run()
.arg("--exact")
.arg("python")
.arg("-c")
.arg("import typing_extensions"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 6 packages in [TIME]
Prepared 2 packages in [TIME]
Installed 2 packages in [TIME]
+ anyio==4.3.0
+ idna==3.6
");
// `--no-group` should remove from the defaults.
uv_snapshot!(context.filters(), context.run()
.arg("--exact")
.arg("--no-group")
.arg("foo")
.arg("python")
.arg("-c")
.arg("import typing_extensions"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 6 packages in [TIME]
Uninstalled 3 packages in [TIME]
- anyio==4.3.0
- idna==3.6
- sniffio==1.3.1
");
// Using `--group` should include the defaults
uv_snapshot!(context.filters(), context.run()
.arg("--exact")
.arg("--group")
.arg("bar")
.arg("python")
.arg("-c")
.arg("import iniconfig"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 6 packages in [TIME]
Prepared 1 package in [TIME]
Installed 4 packages in [TIME]
+ anyio==4.3.0
+ idna==3.6
+ iniconfig==2.0.0
+ sniffio==1.3.1
");
// Using `--all-groups` should include the defaults
uv_snapshot!(context.filters(), context.run()
.arg("--exact")
.arg("--all-groups")
.arg("python")
.arg("-c")
.arg("import iniconfig"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 6 packages in [TIME]
Audited 5 packages in [TIME]
");
// Using `--only-group` should exclude the defaults
uv_snapshot!(context.filters(), context.run()
.arg("--exact")
.arg("--only-group")
.arg("bar")
.arg("python")
.arg("-c")
.arg("import iniconfig"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 6 packages in [TIME]
Uninstalled 4 packages in [TIME]
- anyio==4.3.0
- idna==3.6
- sniffio==1.3.1
- typing-extensions==4.10.0
");
uv_snapshot!(context.filters(), context.run()
.arg("--exact")
.arg("--all-groups")
.arg("python")
.arg("-c")
.arg("import iniconfig"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 6 packages in [TIME]
Installed 4 packages in [TIME]
+ anyio==4.3.0
+ idna==3.6
+ sniffio==1.3.1
+ typing-extensions==4.10.0
");
// Using `--no-default-groups` should exclude all groups.
uv_snapshot!(context.filters(), context.run()
.arg("--exact")
.arg("--no-default-groups")
.arg("python")
.arg("-c")
.arg("import typing_extensions"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 6 packages in [TIME]
Uninstalled 4 packages in [TIME]
- anyio==4.3.0
- idna==3.6
- iniconfig==2.0.0
- sniffio==1.3.1
");
uv_snapshot!(context.filters(), context.run()
.arg("--all-groups")
.arg("python")
.arg("-c")
.arg("import iniconfig"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 6 packages in [TIME]
Installed 4 packages in [TIME]
+ anyio==4.3.0
+ idna==3.6
+ iniconfig==2.0.0
+ sniffio==1.3.1
");
// Using `--no-default-groups` with `--group foo` and `--group bar` should include those
// groups.
uv_snapshot!(context.filters(), context.run()
.arg("--exact")
.arg("--no-default-groups")
.arg("--group")
.arg("foo")
.arg("--group")
.arg("bar")
.arg("python")
.arg("-c")
.arg("import typing_extensions"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 6 packages in [TIME]
Audited 5 packages in [TIME]
");
Ok(())
}
#[test]
fn run_groups_requires_python() -> Result<()> {
let context =
TestContext::new_with_versions(&["3.11", "3.12", "3.13"]).with_filtered_python_sources();
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = ["typing-extensions"]
[dependency-groups]
foo = ["anyio"]
bar = ["iniconfig"]
dev = ["sniffio"]
[tool.uv.dependency-groups]
foo = {requires-python=">=3.14"}
bar = {requires-python=">=3.13"}
dev = {requires-python=">=3.12"}
"#,
)?;
context.lock().assert().success();
// With --no-default-groups only the main requires-python should be consulted
uv_snapshot!(context.filters(), context.run()
.arg("--no-default-groups")
.arg("python").arg("-c").arg("import typing_extensions"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using CPython 3.11.[X] interpreter at: [PYTHON-3.11]
Creating virtual environment at: .venv
Resolved 6 packages in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ typing-extensions==4.10.0
");
// The main requires-python and the default group's requires-python should be consulted
// (This should trigger a version bump)
uv_snapshot!(context.filters(), context.run()
.arg("python").arg("-c").arg("import typing_extensions"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
Removed virtual environment at: .venv
Creating virtual environment at: .venv
Resolved 6 packages in [TIME]
Prepared 1 package in [TIME]
Installed 2 packages in [TIME]
+ sniffio==1.3.1
+ typing-extensions==4.10.0
");
// The main requires-python and "dev" and "bar" requires-python should be consulted
// (This should trigger a version bump)
uv_snapshot!(context.filters(), context.run()
.arg("--group").arg("bar")
.arg("python").arg("-c").arg("import typing_extensions"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using CPython 3.13.[X] interpreter at: [PYTHON-3.13]
Removed virtual environment at: .venv
Creating virtual environment at: .venv
Resolved 6 packages in [TIME]
Prepared 1 package in [TIME]
Installed 3 packages in [TIME]
+ iniconfig==2.0.0
+ sniffio==1.3.1
+ typing-extensions==4.10.0
");
// TMP: Attempt to catch this flake with verbose output
// See https://github.com/astral-sh/uv/issues/14160
let output = context
.run()
.arg("python")
.arg("-c")
.arg("import typing_extensions")
.arg("-vv")
.output()?;
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!stderr.contains("Removed virtual environment"),
"{}",
stderr
);
// Going back to just "dev" we shouldn't churn the venv needlessly
uv_snapshot!(context.filters(), context.run()
.arg("python").arg("-c").arg("import typing_extensions"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 6 packages in [TIME]
Audited 2 packages in [TIME]
");
// Explicitly requesting an in-range python can downgrade
uv_snapshot!(context.filters(), context.run()
.arg("-p").arg("3.12")
.arg("python").arg("-c").arg("import typing_extensions"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
Removed virtual environment at: .venv
Creating virtual environment at: .venv
Resolved 6 packages in [TIME]
Installed 2 packages in [TIME]
+ sniffio==1.3.1
+ typing-extensions==4.10.0
");
// Explicitly requesting an out-of-range python fails
uv_snapshot!(context.filters(), context.run()
.arg("-p").arg("3.11")
.arg("python").arg("-c").arg("import typing_extensions"), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
Using CPython 3.11.[X] interpreter at: [PYTHON-3.11]
error: The requested interpreter resolved to Python 3.11.[X], which is incompatible with the project's Python requirement: `>=3.12` (from `tool.uv.dependency-groups.dev.requires-python`).
");
// Enabling foo we can't find an interpreter
uv_snapshot!(context.filters(), context.run()
.arg("--group").arg("foo")
.arg("python").arg("-c").arg("import typing_extensions"), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: No interpreter found for Python >=3.14 in [PYTHON SOURCES]
");
Ok(())
}
#[test]
fn run_groups_include_requires_python() -> Result<()> {
let context = TestContext::new_with_versions(&["3.11", "3.12", "3.13"]);
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = ["typing-extensions"]
[dependency-groups]
foo = ["anyio"]
bar = ["iniconfig"]
baz = ["iniconfig"]
dev = ["sniffio", {include-group = "foo"}, {include-group = "baz"}]
[tool.uv.dependency-groups]
foo = {requires-python="<3.13"}
bar = {requires-python=">=3.13"}
baz = {requires-python=">=3.12"}
"#,
)?;
context.lock().assert().success();
// With --no-default-groups only the main requires-python should be consulted
uv_snapshot!(context.filters(), context.run()
.arg("--no-default-groups")
.arg("python").arg("-c").arg("import typing_extensions"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using CPython 3.11.[X] interpreter at: [PYTHON-3.11]
Creating virtual environment at: .venv
Resolved 6 packages in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ typing-extensions==4.10.0
");
// The main requires-python and the default group's requires-python should be consulted
// (This should trigger a version bump)
uv_snapshot!(context.filters(), context.run()
.arg("python").arg("-c").arg("import typing_extensions"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
Removed virtual environment at: .venv
Creating virtual environment at: .venv
Resolved 6 packages in [TIME]
Prepared 4 packages in [TIME]
Installed 5 packages in [TIME]
+ anyio==4.3.0
+ idna==3.6
+ iniconfig==2.0.0
+ sniffio==1.3.1
+ typing-extensions==4.10.0
");
// The main requires-python and "dev" and "bar" requires-python should be consulted
// (This should trigger a conflict)
uv_snapshot!(context.filters(), context.run()
.arg("--group").arg("bar")
.arg("python").arg("-c").arg("import typing_extensions"), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Found conflicting Python requirements:
- project: >=3.11
- project:bar: >=3.13
- project:dev: >=3.12, <3.13
");
// Explicitly requesting an out-of-range python fails
uv_snapshot!(context.filters(), context.run()
.arg("-p").arg("3.13")
.arg("python").arg("-c").arg("import typing_extensions"), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
Using CPython 3.13.[X] interpreter at: [PYTHON-3.13]
error: The requested interpreter resolved to Python 3.13.[X], which is incompatible with the project's Python requirement: `==3.12.*` (from `tool.uv.dependency-groups.dev.requires-python`).
");
Ok(())
}
/// Test that a signal n makes the process exit with code 128+n.
#[cfg(unix)]
#[test]
fn exit_status_signal() -> Result<()> {
let context = TestContext::new("3.12");
let script = context.temp_dir.child("segfault.py");
script.write_str(indoc! {r"
import os
os.kill(os.getpid(), 11)
"})?;
let status = context.run().arg(script.path()).status()?;
assert_eq!(status.code().expect("a status code"), 139);
Ok(())
}
#[test]
fn run_repeated() -> Result<()> {
let context = TestContext::new_with_versions(&["3.13", "3.12"]);
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(indoc! { r#"
[project]
name = "foo"
version = "1.0.0"
requires-python = ">=3.11, <4"
dependencies = ["iniconfig"]
"#
})?;
// Import `iniconfig` in the context of the project.
uv_snapshot!(
context.filters(),
context.run().arg("--with").arg("typing-extensions").arg("python").arg("-c").arg("import typing_extensions; import iniconfig"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using CPython 3.13.[X] interpreter at: [PYTHON-3.13]
Creating virtual environment at: .venv
Resolved 2 packages in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ iniconfig==2.0.0
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ typing-extensions==4.10.0
"###);
// Re-running shouldn't require reinstalling `typing-extensions`, since the environment is cached.
uv_snapshot!(
context.filters(),
context.run().arg("--with").arg("typing-extensions").arg("python").arg("-c").arg("import typing_extensions; import iniconfig"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 2 packages in [TIME]
Audited 1 package in [TIME]
Resolved 1 package in [TIME]
"###);
// Re-running as a tool does require reinstalling `typing-extensions`, since the base venv is
// different.
uv_snapshot!(
context.filters(),
context.tool_run().arg("--with").arg("typing-extensions").arg("python").arg("-c").arg("import typing_extensions; import iniconfig"), @r#"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
Resolved 1 package in [TIME]
Installed 1 package in [TIME]
+ typing-extensions==4.10.0
Traceback (most recent call last):
File "<string>", line 1, in <module>
import typing_extensions; import iniconfig
^^^^^^^^^^^^^^^^
ModuleNotFoundError: No module named 'iniconfig'
"#);
Ok(())
}
/// See: <https://github.com/astral-sh/uv/issues/11117>
#[test]
fn run_without_overlay() -> Result<()> {
let context = TestContext::new_with_versions(&["3.13"]);
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(indoc! { r#"
[project]
name = "foo"
version = "1.0.0"
requires-python = ">=3.11, <4"
dependencies = ["iniconfig"]
"#
})?;
// Import `iniconfig` in the context of the project.
uv_snapshot!(
context.filters(),
context.run().arg("--with").arg("typing-extensions").arg("python").arg("-c").arg("import typing_extensions; import iniconfig"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using CPython 3.13.[X] interpreter at: [PYTHON-3.13]
Creating virtual environment at: .venv
Resolved 2 packages in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ iniconfig==2.0.0
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ typing-extensions==4.10.0
"###);
// Import `iniconfig` in the context of a `tool run` command, which should fail. Note that
// typing-extensions gets installed again, because the venv is not shared.
uv_snapshot!(
context.filters(),
context.tool_run().arg("--with").arg("typing-extensions").arg("python").arg("-c").arg("import typing_extensions; import iniconfig"), @r#"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
Resolved 1 package in [TIME]
Installed 1 package in [TIME]
+ typing-extensions==4.10.0
Traceback (most recent call last):
File "<string>", line 1, in <module>
import typing_extensions; import iniconfig
^^^^^^^^^^^^^^^^
ModuleNotFoundError: No module named 'iniconfig'
"#);
// Re-running in the context of the project should reset the overlay.
uv_snapshot!(
context.filters(),
context.run().arg("--with").arg("typing-extensions").arg("python").arg("-c").arg("import typing_extensions; import iniconfig"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 2 packages in [TIME]
Audited 1 package in [TIME]
Resolved 1 package in [TIME]
"###);
Ok(())
}
/// See: <https://github.com/astral-sh/uv/issues/11220>
#[cfg(unix)]
#[test]
fn detect_infinite_recursion() -> Result<()> {
use crate::common::get_bin;
use indoc::formatdoc;
use std::os::unix::fs::PermissionsExt;
let context = TestContext::new("3.12");
let test_script = context.temp_dir.child("main");
test_script.write_str(&formatdoc! { r#"
#!{uv} run
print("Hello, world!")
"#, uv = get_bin().display() })?;
fs_err::set_permissions(test_script.path(), PermissionsExt::from_mode(0o0744))?;
let mut cmd = std::process::Command::new(test_script.as_os_str());
context.add_shared_env(&mut cmd, false);
// Set the max recursion depth to a lower amount to speed up testing.
cmd.env("UV_RUN_MAX_RECURSION_DEPTH", "5");
uv_snapshot!(context.filters(), cmd, @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: `uv run` was recursively invoked 6 times which exceeds the limit of 5.
hint: If you are running a script with `uv run` in the shebang, you may need to include the `--script` flag.
"###);
Ok(())
}
#[test]
fn run_uv_variable() {
let context = TestContext::new("3.12");
// Display the `UV` variable
uv_snapshot!(
context.filters(),
context.run().arg("python").arg("-c").arg("import os; print(os.environ['UV'])"), @r###"
success: true
exit_code: 0
----- stdout -----
[UV]
----- stderr -----
"###);
}
/// Test legacy scripts <https://packaging.python.org/en/latest/guides/distributing-packages-using-setuptools/#scripts>.
///
/// This tests for execution and detection of legacy windows scripts with .bat, .cmd, and .ps1 extensions.
#[cfg(windows)]
#[test]
fn run_windows_legacy_scripts() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
// Use `script-files` which enables legacy scripts packaging.
pyproject_toml.write_str(indoc! { r#"
[project]
name = "foo"
version = "1.0.0"
requires-python = ">=3.8"
dependencies = []
[tool.setuptools]
packages = []
script-files = [
"misc/custom_pydoc.bat",
"misc/custom_pydoc.cmd",
"misc/custom_pydoc.ps1"
]
[build-system]
requires = ["setuptools>=42"]
build-backend = "setuptools.build_meta"
"#
})?;
let custom_pydoc_bat = context.temp_dir.child("misc").child("custom_pydoc.bat");
let custom_pydoc_cmd = context.temp_dir.child("misc").child("custom_pydoc.cmd");
let custom_pydoc_ps1 = context.temp_dir.child("misc").child("custom_pydoc.ps1");
custom_pydoc_bat.write_str("python.exe -m pydoc %*")?;
custom_pydoc_cmd.write_str("python.exe -m pydoc %*")?;
custom_pydoc_ps1.write_str("python.exe -m pydoc $args")?;
uv_snapshot!(context.filters(), context.run(), @r###"
success: false
exit_code: 2
----- stdout -----
Provide a command or script to invoke with `uv run <command>` or `uv run <script>.py`.
The following commands are available in the environment:
- custom_pydoc.bat
- custom_pydoc.cmd
- custom_pydoc.ps1
- pydoc.bat
- python
- pythonw
See `uv run --help` for more information.
----- stderr -----
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ foo==1.0.0 (from file://[TEMP_DIR]/)
"###);
// Test with explicit .bat extension
uv_snapshot!(context.filters(), context.run().arg("custom_pydoc.bat"), @r###"
success: true
exit_code: 0
----- stdout -----
pydoc - the Python documentation tool
pydoc <name> ...
Show text documentation on something. <name> may be the name of a
Python keyword, topic, function, module, or package, or a dotted
reference to a class or function within a module or module in a
package. If <name> contains a '\', it is used as the path to a
Python source file to document. If name is 'keywords', 'topics',
or 'modules', a listing of these things is displayed.
pydoc -k <keyword>
Search for a keyword in the synopsis lines of all available modules.
pydoc -n <hostname>
Start an HTTP server with the given hostname (default: localhost).
pydoc -p <port>
Start an HTTP server on the given port on the local machine. Port
number 0 can be used to get an arbitrary unused port.
pydoc -b
Start an HTTP server on an arbitrary unused port and open a web browser
to interactively browse documentation. This option can be used in
combination with -n and/or -p.
pydoc -w <name> ...
Write out the HTML documentation for a module to a file in the current
directory. If <name> contains a '\', it is treated as a filename; if
it names a directory, documentation is written for all the contents.
----- stderr -----
Resolved 1 package in [TIME]
Audited 1 package in [TIME]
"###);
// Test with explicit .cmd extension
uv_snapshot!(context.filters(), context.run().arg("custom_pydoc.cmd"), @r###"
success: true
exit_code: 0
----- stdout -----
pydoc - the Python documentation tool
pydoc <name> ...
Show text documentation on something. <name> may be the name of a
Python keyword, topic, function, module, or package, or a dotted
reference to a class or function within a module or module in a
package. If <name> contains a '\', it is used as the path to a
Python source file to document. If name is 'keywords', 'topics',
or 'modules', a listing of these things is displayed.
pydoc -k <keyword>
Search for a keyword in the synopsis lines of all available modules.
pydoc -n <hostname>
Start an HTTP server with the given hostname (default: localhost).
pydoc -p <port>
Start an HTTP server on the given port on the local machine. Port
number 0 can be used to get an arbitrary unused port.
pydoc -b
Start an HTTP server on an arbitrary unused port and open a web browser
to interactively browse documentation. This option can be used in
combination with -n and/or -p.
pydoc -w <name> ...
Write out the HTML documentation for a module to a file in the current
directory. If <name> contains a '\', it is treated as a filename; if
it names a directory, documentation is written for all the contents.
----- stderr -----
Resolved 1 package in [TIME]
Audited 1 package in [TIME]
"###);
// Test with explicit .ps1 extension
uv_snapshot!(context.filters(), context.run().arg("custom_pydoc.ps1"), @r###"
success: true
exit_code: 0
----- stdout -----
pydoc - the Python documentation tool
pydoc <name> ...
Show text documentation on something. <name> may be the name of a
Python keyword, topic, function, module, or package, or a dotted
reference to a class or function within a module or module in a
package. If <name> contains a '\', it is used as the path to a
Python source file to document. If name is 'keywords', 'topics',
or 'modules', a listing of these things is displayed.
pydoc -k <keyword>
Search for a keyword in the synopsis lines of all available modules.
pydoc -n <hostname>
Start an HTTP server with the given hostname (default: localhost).
pydoc -p <port>
Start an HTTP server on the given port on the local machine. Port
number 0 can be used to get an arbitrary unused port.
pydoc -b
Start an HTTP server on an arbitrary unused port and open a web browser
to interactively browse documentation. This option can be used in
combination with -n and/or -p.
pydoc -w <name> ...
Write out the HTML documentation for a module to a file in the current
directory. If <name> contains a '\', it is treated as a filename; if
it names a directory, documentation is written for all the contents.
----- stderr -----
Resolved 1 package in [TIME]
Audited 1 package in [TIME]
"###);
// Test without explicit extension (.ps1 should be used) as there's no .exe available.
uv_snapshot!(context.filters(), context.run().arg("custom_pydoc"), @r###"
success: true
exit_code: 0
----- stdout -----
pydoc - the Python documentation tool
pydoc <name> ...
Show text documentation on something. <name> may be the name of a
Python keyword, topic, function, module, or package, or a dotted
reference to a class or function within a module or module in a
package. If <name> contains a '\', it is used as the path to a
Python source file to document. If name is 'keywords', 'topics',
or 'modules', a listing of these things is displayed.
pydoc -k <keyword>
Search for a keyword in the synopsis lines of all available modules.
pydoc -n <hostname>
Start an HTTP server with the given hostname (default: localhost).
pydoc -p <port>
Start an HTTP server on the given port on the local machine. Port
number 0 can be used to get an arbitrary unused port.
pydoc -b
Start an HTTP server on an arbitrary unused port and open a web browser
to interactively browse documentation. This option can be used in
combination with -n and/or -p.
pydoc -w <name> ...
Write out the HTML documentation for a module to a file in the current
directory. If <name> contains a '\', it is treated as a filename; if
it names a directory, documentation is written for all the contents.
----- stderr -----
Resolved 1 package in [TIME]
Audited 1 package in [TIME]
"###);
Ok(())
}
/// If a `--with` requirement overlaps with a locked script requirement, respect the lockfile as a
/// preference.
///
/// See: <https://github.com/astral-sh/uv/issues/13173>
#[test]
fn run_pep723_script_with_constraints_lock() -> Result<()> {
let context = TestContext::new("3.12");
let test_script = context.temp_dir.child("main.py");
test_script.write_str(indoc! { r#"
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "iniconfig<2",
# ]
# ///
import iniconfig
print("Hello, world!")
"#
})?;
// Explicitly lock the script.
uv_snapshot!(context.filters(), context.lock().arg("--script").arg("main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 1 package in [TIME]
"###);
let lock = context.read("main.py.lock");
insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(
lock, @r#"
version = 1
revision = 2
requires-python = ">=3.11"
[options]
exclude-newer = "2024-03-25T00:00:00Z"
[manifest]
requirements = [{ name = "iniconfig", specifier = "<2" }]
[[package]]
name = "iniconfig"
version = "1.1.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/23/a2/97899f6bd0e873fed3a7e67ae8d3a08b21799430fb4da15cfedf10d6e2c2/iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32", size = 8104, upload-time = "2020-10-14T10:20:18.572Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9b/dd/b3c12c6d707058fa947864b67f0c4e0c39ef8610988d7baea9578f3c48f3/iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", size = 4990, upload-time = "2020-10-16T17:37:23.05Z" },
]
"#
);
});
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(indoc! { r#"
[project]
name = "foo"
version = "1.0.0"
requires-python = ">=3.10"
dependencies = [
"iniconfig",
]
"#
})?;
uv_snapshot!(context.filters(), context.run().arg("--with").arg(".").arg("main.py"), @r"
success: true
exit_code: 0
----- stdout -----
Hello, world!
----- stderr -----
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ iniconfig==1.1.1
Resolved 2 packages in [TIME]
Prepared 1 package in [TIME]
Installed 2 packages in [TIME]
+ foo==1.0.0 (from file://[TEMP_DIR]/)
+ iniconfig==1.1.1
");
Ok(())
}
/// If a `--with` requirement overlaps with a non-locked script requirement, respect the environment
/// site-packages as preferences.
///
/// See: <https://github.com/astral-sh/uv/issues/13173>
#[test]
fn run_pep723_script_with_constraints() -> Result<()> {
let context = TestContext::new("3.12");
let test_script = context.temp_dir.child("main.py");
test_script.write_str(indoc! { r#"
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "iniconfig<2",
# ]
# ///
import iniconfig
print("Hello, world!")
"#
})?;
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(indoc! { r#"
[project]
name = "foo"
version = "1.0.0"
requires-python = ">=3.10"
dependencies = [
"iniconfig",
]
"#
})?;
uv_snapshot!(context.filters(), context.run().arg("--with").arg(".").arg("main.py"), @r"
success: true
exit_code: 0
----- stdout -----
Hello, world!
----- stderr -----
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ iniconfig==1.1.1
Resolved 2 packages in [TIME]
Prepared 1 package in [TIME]
Installed 2 packages in [TIME]
+ foo==1.0.0 (from file://[TEMP_DIR]/)
+ iniconfig==1.1.1
");
Ok(())
}
#[test]
fn run_no_sync_incompatible_python() -> Result<()> {
let context = TestContext::new_with_versions(&["3.12", "3.11", "3.9"]);
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(indoc! { r#"
[project]
name = "foo"
version = "1.0.0"
requires-python = ">=3.12"
dependencies = [
"iniconfig"
]
"#
})?;
let test_script = context.temp_dir.child("main.py");
test_script.write_str(indoc! { r#"
import iniconfig
print("Hello, world!")
"#
})?;
uv_snapshot!(context.filters(), context.run().arg("main.py"), @r"
success: true
exit_code: 0
----- stdout -----
Hello, world!
----- stderr -----
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
Creating virtual environment at: .venv
Resolved 2 packages in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ iniconfig==2.0.0
");
uv_snapshot!(context.filters(), context.run().arg("--no-sync").arg("--python").arg("3.9").arg("main.py"), @r"
success: true
exit_code: 0
----- stdout -----
Hello, world!
----- stderr -----
warning: Using incompatible environment (`.venv`) due to `--no-sync` (The project environment's Python version does not satisfy the request: `Python 3.9`)
");
Ok(())
}
#[test]
fn run_python_preference_no_project() {
let context =
TestContext::new_with_versions(&["3.12", "3.11"]).with_versions_as_managed(&["3.12"]);
context.venv().assert().success();
uv_snapshot!(context.filters(), context.run().arg("python").arg("--version"), @r"
success: true
exit_code: 0
----- stdout -----
Python 3.12.[X]
----- stderr -----
");
uv_snapshot!(context.filters(), context.run().arg("--managed-python").arg("python").arg("--version"), @r"
success: true
exit_code: 0
----- stdout -----
Python 3.12.[X]
----- stderr -----
");
// `VIRTUAL_ENV` is set here, so we'll ignore the flag
uv_snapshot!(context.filters(), context.run().arg("--no-managed-python").arg("python").arg("--version"), @r"
success: true
exit_code: 0
----- stdout -----
Python 3.12.[X]
----- stderr -----
");
// If we remove the `VIRTUAL_ENV` variable, we should get the unmanaged Python
uv_snapshot!(context.filters(), context.run().arg("--no-managed-python").arg("python").arg("--version").env_remove("VIRTUAL_ENV"), @r"
success: true
exit_code: 0
----- stdout -----
Python 3.11.[X]
----- stderr -----
");
}