mirror of
https://github.com/astral-sh/uv.git
synced 2025-11-01 20:31:12 +00:00
## One-liner
Relative find-links configuration to local path from a pyproject.toml or
uv.toml is now relative to the config file
## Summary
### Background
One can configure find-links in a `pyproject.toml` or `uv.toml` file,
which are located from the cli arg, system directory, user directory, or
by traversing parent directories until one is encountered.
This PR addresses the following scenario:
- A project directory which includes a `pyproject.toml` or `uv.toml`
file
- The config file includes a `find-links` option. (eg under `[tool.uv]`
for `pyproject.toml`)
- The `find-links` option is configured to point to a local subdirectory
in the project: `packages/`
- There is a subdirectory called `subdir`, which is the current working
directory
- I run `uv run my_script.py`. This will locate the `pyproject.toml` in
the parent directory
### Current Behavior
- uv tries to use the path `subdir/packages/` to find packages, and
fails.
### New Behavior
- uv tries to use the path `packages/` to find the packages, and
succeeds
- Specifically, any relative local find-links path will resolve to be
relative to the configuration file.
### Why is this behavior change OK?
- I believe no one depends on the behavior that a relative find-links
when running in a subdir will refer to different directories each time
- Thus this change only allows a more common use case which didn't work
previously.
## Test Plan
- I re-created the setup mentioned above:
```
UvTest/
├── packages/
│ ├── colorama-0.4.6-py2.py3-none-any.whl
│ └── tqdm-4.67.1-py3-none-any.whl
├── subdir/
│ └── my_script.py
└── pyproject.toml
```
```toml
# pyproject.toml
[project]
name = "uvtest"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"tqdm>=4.67.1",
]
[tool.uv]
offline = true
no-index = true
find-links = ["packages/"]
```
- With working directory under `subdir`, previously, running `uv sync
--offline` would fail resolving the tdqm package, and after the change
it succeeds.
- Additionally, one can use `uv sync --show-settings` to show the
actually-resolved settings - now having the desired path in
`flat_index.url.path`
## Alternative designs considered
- I considered modifying the `impl Deserialize for IndexUrl` to parse
ahead of time directly with a base directory by having a custom
`Deserializer` with a base dir field, but it seems to contradict the
design of the serde `Deserialize` trait - which should work with all
`Deserializer`s
## Future work
- Support for adjusting all other local-relative paths in `Options`
would be desired, but is out of scope for the current PR.
---------
Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
6163 lines
174 KiB
Rust
6163 lines
174 KiB
Rust
use anyhow::Result;
|
||
use assert_cmd::prelude::*;
|
||
use assert_fs::{fixture::ChildPath, prelude::*};
|
||
use indoc::{formatdoc, indoc};
|
||
use insta::assert_snapshot;
|
||
|
||
use crate::common::{download_to_disk, uv_snapshot, venv_bin_path, TestContext};
|
||
use predicates::prelude::predicate;
|
||
use tempfile::tempdir_in;
|
||
use uv_fs::Simplified;
|
||
use uv_static::EnvVars;
|
||
|
||
#[test]
|
||
fn 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 = ["iniconfig"]
|
||
"#,
|
||
)?;
|
||
|
||
// Running `uv sync` should generate a lockfile.
|
||
uv_snapshot!(context.filters(), context.sync(), @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
|
||
"###);
|
||
|
||
assert!(context.temp_dir.child("uv.lock").exists());
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn 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"]
|
||
"#,
|
||
)?;
|
||
|
||
// Running with `--locked` should error, if no lockfile is present.
|
||
uv_snapshot!(context.filters(), context.sync().arg("--locked"), @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");
|
||
|
||
// Update the requirements.
|
||
pyproject_toml.write_str(
|
||
r#"
|
||
[project]
|
||
name = "project"
|
||
version = "0.1.0"
|
||
requires-python = ">=3.12"
|
||
dependencies = ["iniconfig"]
|
||
"#,
|
||
)?;
|
||
|
||
// Running with `--locked` should error.
|
||
uv_snapshot!(context.filters(), context.sync().arg("--locked"), @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);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn 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"]
|
||
"#,
|
||
)?;
|
||
|
||
// Running with `--frozen` should error, if no lockfile is present.
|
||
uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @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"]
|
||
"#,
|
||
)?;
|
||
|
||
// Running with `--frozen` should install the stale lockfile.
|
||
uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Prepared 3 packages in [TIME]
|
||
Installed 3 packages in [TIME]
|
||
+ anyio==3.7.0
|
||
+ idna==3.6
|
||
+ sniffio==1.3.1
|
||
"###);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn empty() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||
pyproject_toml.write_str(
|
||
r"
|
||
[tool.uv.workspace]
|
||
members = []
|
||
",
|
||
)?;
|
||
|
||
// Running `uv sync` should generate an empty lockfile.
|
||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
warning: No `requires-python` value found in the workspace. Defaulting to `>=3.12`.
|
||
Resolved in [TIME]
|
||
Audited in [TIME]
|
||
"###);
|
||
|
||
assert!(context.temp_dir.child("uv.lock").exists());
|
||
|
||
// Running `uv sync` again should succeed.
|
||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
warning: No `requires-python` value found in the workspace. Defaulting to `>=3.12`.
|
||
Resolved in [TIME]
|
||
Audited in [TIME]
|
||
"###);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Sync an individual package within a workspace.
|
||
#[test]
|
||
fn package() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||
pyproject_toml.write_str(
|
||
r#"
|
||
[project]
|
||
name = "root"
|
||
version = "0.1.0"
|
||
requires-python = ">=3.12"
|
||
dependencies = ["child", "anyio>3"]
|
||
|
||
[tool.uv.sources]
|
||
child = { workspace = true }
|
||
|
||
[tool.uv.workspace]
|
||
members = ["child"]
|
||
"#,
|
||
)?;
|
||
|
||
let src = context.temp_dir.child("src").child("albatross");
|
||
src.create_dir_all()?;
|
||
|
||
let init = src.child("__init__.py");
|
||
init.touch()?;
|
||
|
||
let child = context.temp_dir.child("child");
|
||
fs_err::create_dir_all(&child)?;
|
||
|
||
let pyproject_toml = child.child("pyproject.toml");
|
||
pyproject_toml.write_str(
|
||
r#"
|
||
[project]
|
||
name = "child"
|
||
version = "0.1.0"
|
||
requires-python = ">=3.12"
|
||
dependencies = ["iniconfig>=1"]
|
||
|
||
[build-system]
|
||
requires = ["setuptools>=42"]
|
||
build-backend = "setuptools.build_meta"
|
||
"#,
|
||
)?;
|
||
|
||
let src = child.child("src").child("albatross");
|
||
src.create_dir_all()?;
|
||
|
||
let init = src.child("__init__.py");
|
||
init.touch()?;
|
||
|
||
uv_snapshot!(context.filters(), context.sync().arg("--package").arg("child"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 6 packages in [TIME]
|
||
Prepared 2 packages in [TIME]
|
||
Installed 2 packages in [TIME]
|
||
+ child==0.1.0 (from file://[TEMP_DIR]/child)
|
||
+ iniconfig==2.0.0
|
||
"###);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Ensure that we use the maximum Python version when a workspace contains mixed requirements.
|
||
#[test]
|
||
fn mixed_requires_python() -> Result<()> {
|
||
let context = TestContext::new_with_versions(&["3.8", "3.12"]);
|
||
|
||
// Create a workspace root with a minimum Python requirement of Python 3.12.
|
||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||
pyproject_toml.write_str(
|
||
r#"
|
||
[project]
|
||
name = "albatross"
|
||
version = "0.1.0"
|
||
requires-python = ">=3.12"
|
||
dependencies = ["bird-feeder", "anyio>3"]
|
||
|
||
[tool.uv.sources]
|
||
bird-feeder = { workspace = true }
|
||
|
||
[tool.uv.workspace]
|
||
members = ["packages/*"]
|
||
"#,
|
||
)?;
|
||
|
||
let src = context.temp_dir.child("src").child("albatross");
|
||
src.create_dir_all()?;
|
||
|
||
let init = src.child("__init__.py");
|
||
init.touch()?;
|
||
|
||
// Create a child with a minimum Python requirement of Python 3.8.
|
||
let child = context.temp_dir.child("packages").child("bird-feeder");
|
||
child.create_dir_all()?;
|
||
|
||
let src = context.temp_dir.child("src").child("bird_feeder");
|
||
src.create_dir_all()?;
|
||
|
||
let init = src.child("__init__.py");
|
||
init.touch()?;
|
||
|
||
let pyproject_toml = child.child("pyproject.toml");
|
||
pyproject_toml.write_str(
|
||
r#"
|
||
[project]
|
||
name = "bird-feeder"
|
||
version = "0.1.0"
|
||
requires-python = ">=3.8"
|
||
|
||
[build-system]
|
||
requires = ["setuptools>=42"]
|
||
build-backend = "setuptools.build_meta"
|
||
"#,
|
||
)?;
|
||
|
||
// Running `uv sync` should succeed, locking for Python 3.12.
|
||
uv_snapshot!(context.filters(), context.sync().arg("-p").arg("3.12"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- 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==4.3.0
|
||
+ bird-feeder==0.1.0 (from file://[TEMP_DIR]/packages/bird-feeder)
|
||
+ idna==3.6
|
||
+ sniffio==1.3.1
|
||
"###);
|
||
|
||
// Running `uv sync` again should fail.
|
||
uv_snapshot!(context.filters(), context.sync().arg("-p").arg("3.8"), @r###"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Using CPython 3.8.[X] interpreter at: [PYTHON-3.8]
|
||
error: The requested interpreter resolved to Python 3.8.[X], which is incompatible with the project's Python requirement: `>=3.12`. However, a workspace member (`bird-feeder`) supports Python >=3.8. To install the workspace member on its own, navigate to `packages/bird-feeder`, then run `uv venv --python 3.8.[X]` followed by `uv pip install -e .`.
|
||
"###);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Sync development dependencies in a (legacy) non-project workspace root.
|
||
#[test]
|
||
fn sync_legacy_non_project_dev_dependencies() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||
pyproject_toml.write_str(
|
||
r#"
|
||
[tool.uv]
|
||
dev-dependencies = ["anyio>3", "requests[socks]", "typing-extensions ; sys_platform == ''"]
|
||
|
||
[tool.uv.workspace]
|
||
members = ["child"]
|
||
"#,
|
||
)?;
|
||
|
||
let src = context.temp_dir.child("src").child("albatross");
|
||
src.create_dir_all()?;
|
||
|
||
let init = src.child("__init__.py");
|
||
init.touch()?;
|
||
|
||
let child = context.temp_dir.child("child");
|
||
fs_err::create_dir_all(&child)?;
|
||
|
||
let pyproject_toml = child.child("pyproject.toml");
|
||
pyproject_toml.write_str(
|
||
r#"
|
||
[project]
|
||
name = "child"
|
||
version = "0.1.0"
|
||
requires-python = ">=3.12"
|
||
dependencies = ["iniconfig>=1"]
|
||
|
||
[build-system]
|
||
requires = ["setuptools>=42"]
|
||
build-backend = "setuptools.build_meta"
|
||
"#,
|
||
)?;
|
||
|
||
let src = child.child("src").child("albatross");
|
||
src.create_dir_all()?;
|
||
|
||
let init = src.child("__init__.py");
|
||
init.touch()?;
|
||
|
||
// Syncing with `--no-dev` should omit all dependencies except `iniconfig`.
|
||
uv_snapshot!(context.filters(), context.sync().arg("--no-dev"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 11 packages in [TIME]
|
||
Prepared 2 packages in [TIME]
|
||
Installed 2 packages in [TIME]
|
||
+ child==0.1.0 (from file://[TEMP_DIR]/child)
|
||
+ iniconfig==2.0.0
|
||
"###);
|
||
|
||
// Syncing without `--no-dev` should include `anyio`, `requests`, `pysocks`, and their
|
||
// dependencies, but not `typing-extensions`.
|
||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 11 packages in [TIME]
|
||
Prepared 8 packages in [TIME]
|
||
Installed 8 packages in [TIME]
|
||
+ anyio==4.3.0
|
||
+ certifi==2024.2.2
|
||
+ charset-normalizer==3.3.2
|
||
+ idna==3.6
|
||
+ pysocks==1.7.1
|
||
+ requests==2.31.0
|
||
+ sniffio==1.3.1
|
||
+ urllib3==2.2.1
|
||
"###);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Sync development dependencies in a (legacy) non-project workspace root with `--frozen`.
|
||
#[test]
|
||
fn sync_legacy_non_project_frozen() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||
pyproject_toml.write_str(
|
||
r#"
|
||
[tool.uv.workspace]
|
||
members = ["foo", "bar"]
|
||
"#,
|
||
)?;
|
||
|
||
context
|
||
.temp_dir
|
||
.child("foo")
|
||
.child("pyproject.toml")
|
||
.write_str(
|
||
r#"
|
||
[project]
|
||
name = "foo"
|
||
version = "0.1.0"
|
||
requires-python = ">=3.12"
|
||
dependencies = ["iniconfig>=1"]
|
||
"#,
|
||
)?;
|
||
|
||
context
|
||
.temp_dir
|
||
.child("bar")
|
||
.child("pyproject.toml")
|
||
.write_str(
|
||
r#"
|
||
[project]
|
||
name = "bar"
|
||
version = "0.1.0"
|
||
requires-python = ">=3.12"
|
||
dependencies = ["typing-extensions>=4"]
|
||
"#,
|
||
)?;
|
||
|
||
context.lock().assert().success();
|
||
|
||
uv_snapshot!(context.filters(), context.sync().arg("--frozen").arg("--package").arg("foo"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Prepared 1 package in [TIME]
|
||
Installed 1 package in [TIME]
|
||
+ iniconfig==2.0.0
|
||
"###);
|
||
|
||
uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Prepared 1 package in [TIME]
|
||
Installed 1 package in [TIME]
|
||
+ typing-extensions==4.10.0
|
||
"###);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Sync development dependencies in a (legacy) non-project workspace root.
|
||
#[test]
|
||
fn sync_legacy_non_project_group() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||
pyproject_toml.write_str(
|
||
r#"
|
||
[dependency-groups]
|
||
foo = ["anyio"]
|
||
bar = ["typing-extensions"]
|
||
|
||
[tool.uv.workspace]
|
||
members = ["child"]
|
||
"#,
|
||
)?;
|
||
|
||
let src = context.temp_dir.child("src").child("albatross");
|
||
src.create_dir_all()?;
|
||
|
||
let init = src.child("__init__.py");
|
||
init.touch()?;
|
||
|
||
let child = context.temp_dir.child("child");
|
||
fs_err::create_dir_all(&child)?;
|
||
|
||
let pyproject_toml = child.child("pyproject.toml");
|
||
pyproject_toml.write_str(
|
||
r#"
|
||
[project]
|
||
name = "child"
|
||
version = "0.1.0"
|
||
requires-python = ">=3.12"
|
||
dependencies = ["iniconfig>=1"]
|
||
|
||
[dependency-groups]
|
||
baz = ["typing-extensions"]
|
||
|
||
[build-system]
|
||
requires = ["setuptools>=42"]
|
||
build-backend = "setuptools.build_meta"
|
||
"#,
|
||
)?;
|
||
|
||
let src = child.child("src").child("albatross");
|
||
src.create_dir_all()?;
|
||
|
||
let init = src.child("__init__.py");
|
||
init.touch()?;
|
||
|
||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 6 packages in [TIME]
|
||
Prepared 2 packages in [TIME]
|
||
Installed 2 packages in [TIME]
|
||
+ child==0.1.0 (from file://[TEMP_DIR]/child)
|
||
+ iniconfig==2.0.0
|
||
"###);
|
||
|
||
uv_snapshot!(context.filters(), context.sync().arg("--group").arg("foo"), @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
|
||
"###);
|
||
|
||
uv_snapshot!(context.filters(), context.sync().arg("--only-group").arg("bar"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 6 packages in [TIME]
|
||
Prepared 1 package in [TIME]
|
||
Uninstalled 5 packages in [TIME]
|
||
Installed 1 package in [TIME]
|
||
- anyio==4.3.0
|
||
- child==0.1.0 (from file://[TEMP_DIR]/child)
|
||
- idna==3.6
|
||
- iniconfig==2.0.0
|
||
- sniffio==1.3.1
|
||
+ typing-extensions==4.10.0
|
||
"###);
|
||
|
||
uv_snapshot!(context.filters(), context.sync().arg("--group").arg("baz"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 6 packages in [TIME]
|
||
Installed 2 packages in [TIME]
|
||
+ child==0.1.0 (from file://[TEMP_DIR]/child)
|
||
+ iniconfig==2.0.0
|
||
"###);
|
||
|
||
uv_snapshot!(context.filters(), context.sync().arg("--group").arg("bop"), @r###"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: Group `bop` is not defined in any project's `dependency-group` table
|
||
"###);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Sync development dependencies in a (legacy) non-project workspace root with `--frozen`.
|
||
///
|
||
/// Modify the `pyproject.toml` after locking.
|
||
#[test]
|
||
fn sync_legacy_non_project_frozen_modification() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||
pyproject_toml.write_str(
|
||
r#"
|
||
[tool.uv.workspace]
|
||
members = []
|
||
|
||
[dependency-groups]
|
||
async = ["anyio"]
|
||
"#,
|
||
)?;
|
||
|
||
context.lock().assert().success();
|
||
|
||
uv_snapshot!(context.filters(), context.sync().arg("--frozen").arg("--group").arg("async"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Prepared 3 packages in [TIME]
|
||
Installed 3 packages in [TIME]
|
||
+ anyio==4.3.0
|
||
+ idna==3.6
|
||
+ sniffio==1.3.1
|
||
"###);
|
||
|
||
// Modify the "live" dependency groups.
|
||
pyproject_toml.write_str(
|
||
r#"
|
||
[tool.uv.workspace]
|
||
members = []
|
||
|
||
[dependency-groups]
|
||
async = ["iniconfig"]
|
||
"#,
|
||
)?;
|
||
|
||
// This should succeed.
|
||
uv_snapshot!(context.filters(), context.sync().arg("--frozen").arg("--group").arg("async"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Audited 3 packages in [TIME]
|
||
"###);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Use a `pip install` step to pre-install build dependencies for `--no-build-isolation`.
|
||
#[test]
|
||
fn sync_build_isolation() -> 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 = ["source-distribution @ https://files.pythonhosted.org/packages/10/1f/57aa4cce1b1abf6b433106676e15f9fa2c92ed2bd4cf77c3b50a9e9ac773/source_distribution-0.0.1.tar.gz"]
|
||
"#,
|
||
)?;
|
||
|
||
// Running `uv sync` should fail (but it could fail when building the root project, or when
|
||
// building `source-distribution`).
|
||
context
|
||
.sync()
|
||
.arg("--no-build-isolation")
|
||
.assert()
|
||
.failure();
|
||
|
||
// Install `setuptools` (for the root project) plus `hatchling` (for `source-distribution`).
|
||
uv_snapshot!(context.filters(), context.pip_install().arg("wheel").arg("setuptools").arg("hatchling"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 7 packages in [TIME]
|
||
Prepared 7 packages in [TIME]
|
||
Installed 7 packages in [TIME]
|
||
+ hatchling==1.22.4
|
||
+ packaging==24.0
|
||
+ pathspec==0.12.1
|
||
+ pluggy==1.4.0
|
||
+ setuptools==69.2.0
|
||
+ trove-classifiers==2024.3.3
|
||
+ wheel==0.43.0
|
||
"###);
|
||
|
||
// Running `uv sync` should succeed.
|
||
uv_snapshot!(context.filters(), context.sync().arg("--no-build-isolation"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 2 packages in [TIME]
|
||
Prepared 1 package in [TIME]
|
||
Uninstalled 7 packages in [TIME]
|
||
Installed 1 package in [TIME]
|
||
- hatchling==1.22.4
|
||
- packaging==24.0
|
||
- pathspec==0.12.1
|
||
- pluggy==1.4.0
|
||
- setuptools==69.2.0
|
||
+ source-distribution==0.0.1 (from https://files.pythonhosted.org/packages/10/1f/57aa4cce1b1abf6b433106676e15f9fa2c92ed2bd4cf77c3b50a9e9ac773/source_distribution-0.0.1.tar.gz)
|
||
- trove-classifiers==2024.3.3
|
||
- wheel==0.43.0
|
||
"###);
|
||
|
||
assert!(context.temp_dir.child("uv.lock").exists());
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Use a `pip install` step to pre-install build dependencies for `--no-build-isolation-package`.
|
||
#[test]
|
||
fn sync_build_isolation_package() -> 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 = [
|
||
"source-distribution @ https://files.pythonhosted.org/packages/10/1f/57aa4cce1b1abf6b433106676e15f9fa2c92ed2bd4cf77c3b50a9e9ac773/source_distribution-0.0.1.tar.gz",
|
||
]
|
||
|
||
[build-system]
|
||
requires = ["setuptools >= 40.9.0"]
|
||
build-backend = "setuptools.build_meta"
|
||
"#,
|
||
)?;
|
||
|
||
// Running `uv sync` should fail for iniconfig.
|
||
let filters = std::iter::once((r"exit code: 1", "exit status: 1"))
|
||
.chain(context.filters())
|
||
.collect::<Vec<_>>();
|
||
uv_snapshot!(filters, context.sync().arg("--no-build-isolation-package").arg("source-distribution"), @r###"
|
||
success: false
|
||
exit_code: 1
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 2 packages in [TIME]
|
||
× Failed to build `source-distribution @ https://files.pythonhosted.org/packages/10/1f/57aa4cce1b1abf6b433106676e15f9fa2c92ed2bd4cf77c3b50a9e9ac773/source_distribution-0.0.1.tar.gz`
|
||
├─▶ The build backend returned an error
|
||
╰─▶ Call to `hatchling.build.build_wheel` failed (exit status: 1)
|
||
|
||
[stderr]
|
||
Traceback (most recent call last):
|
||
File "<string>", line 8, in <module>
|
||
ModuleNotFoundError: No module named 'hatchling'
|
||
|
||
hint: This usually indicates a problem with the package or the build environment.
|
||
help: `source-distribution` was included because `project` (v0.1.0) depends on `source-distribution`
|
||
"###);
|
||
|
||
// Install `hatchling` for `source-distribution`.
|
||
uv_snapshot!(context.filters(), context.pip_install().arg("hatchling"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 5 packages in [TIME]
|
||
Prepared 5 packages in [TIME]
|
||
Installed 5 packages in [TIME]
|
||
+ hatchling==1.22.4
|
||
+ packaging==24.0
|
||
+ pathspec==0.12.1
|
||
+ pluggy==1.4.0
|
||
+ trove-classifiers==2024.3.3
|
||
"###);
|
||
|
||
// Running `uv sync` should succeed.
|
||
uv_snapshot!(context.filters(), context.sync().arg("--no-build-isolation-package").arg("source-distribution"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 2 packages in [TIME]
|
||
Prepared 2 packages in [TIME]
|
||
Uninstalled 5 packages in [TIME]
|
||
Installed 2 packages in [TIME]
|
||
- hatchling==1.22.4
|
||
- packaging==24.0
|
||
- pathspec==0.12.1
|
||
- pluggy==1.4.0
|
||
+ project==0.1.0 (from file://[TEMP_DIR]/)
|
||
+ source-distribution==0.0.1 (from https://files.pythonhosted.org/packages/10/1f/57aa4cce1b1abf6b433106676e15f9fa2c92ed2bd4cf77c3b50a9e9ac773/source_distribution-0.0.1.tar.gz)
|
||
- trove-classifiers==2024.3.3
|
||
"###);
|
||
|
||
assert!(context.temp_dir.child("uv.lock").exists());
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Use dedicated extra groups to install dependencies for `--no-build-isolation-package`.
|
||
#[test]
|
||
fn sync_build_isolation_extra() -> Result<()> {
|
||
let context = TestContext::new("3.12").with_filtered_counts();
|
||
|
||
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 = []
|
||
|
||
[project.optional-dependencies]
|
||
build = ["hatchling"]
|
||
compile = ["source-distribution @ https://files.pythonhosted.org/packages/10/1f/57aa4cce1b1abf6b433106676e15f9fa2c92ed2bd4cf77c3b50a9e9ac773/source_distribution-0.0.1.tar.gz"]
|
||
|
||
[build-system]
|
||
requires = ["setuptools >= 40.9.0"]
|
||
build-backend = "setuptools.build_meta"
|
||
|
||
[tool.uv]
|
||
no-build-isolation-package = ["source-distribution"]
|
||
"#,
|
||
)?;
|
||
|
||
// Running `uv sync` should fail for the `compile` extra.
|
||
let filters = std::iter::once((r"exit code: 1", "exit status: 1"))
|
||
.chain(context.filters())
|
||
.collect::<Vec<_>>();
|
||
uv_snapshot!(&filters, context.sync().arg("--extra").arg("compile"), @r###"
|
||
success: false
|
||
exit_code: 1
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved [N] packages in [TIME]
|
||
× Failed to build `source-distribution @ https://files.pythonhosted.org/packages/10/1f/57aa4cce1b1abf6b433106676e15f9fa2c92ed2bd4cf77c3b50a9e9ac773/source_distribution-0.0.1.tar.gz`
|
||
├─▶ The build backend returned an error
|
||
╰─▶ Call to `hatchling.build.build_wheel` failed (exit status: 1)
|
||
|
||
[stderr]
|
||
Traceback (most recent call last):
|
||
File "<string>", line 8, in <module>
|
||
ModuleNotFoundError: No module named 'hatchling'
|
||
|
||
hint: This usually indicates a problem with the package or the build environment.
|
||
help: `source-distribution` was included because `project[compile]` (v0.1.0) depends on `source-distribution`
|
||
"###);
|
||
|
||
// Running `uv sync` with `--all-extras` should also fail.
|
||
uv_snapshot!(&filters, context.sync().arg("--all-extras"), @r###"
|
||
success: false
|
||
exit_code: 1
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved [N] packages in [TIME]
|
||
× Failed to build `source-distribution @ https://files.pythonhosted.org/packages/10/1f/57aa4cce1b1abf6b433106676e15f9fa2c92ed2bd4cf77c3b50a9e9ac773/source_distribution-0.0.1.tar.gz`
|
||
├─▶ The build backend returned an error
|
||
╰─▶ Call to `hatchling.build.build_wheel` failed (exit status: 1)
|
||
|
||
[stderr]
|
||
Traceback (most recent call last):
|
||
File "<string>", line 8, in <module>
|
||
ModuleNotFoundError: No module named 'hatchling'
|
||
|
||
hint: This usually indicates a problem with the package or the build environment.
|
||
help: `source-distribution` was included because `project[compile]` (v0.1.0) depends on `source-distribution`
|
||
"###);
|
||
|
||
// Install the build dependencies.
|
||
uv_snapshot!(context.filters(), context.sync().arg("--extra").arg("build"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved [N] packages in [TIME]
|
||
Prepared [N] packages in [TIME]
|
||
Installed [N] packages in [TIME]
|
||
+ hatchling==1.22.4
|
||
+ packaging==24.0
|
||
+ pathspec==0.12.1
|
||
+ pluggy==1.4.0
|
||
+ project==0.1.0 (from file://[TEMP_DIR]/)
|
||
+ trove-classifiers==2024.3.3
|
||
"###);
|
||
|
||
// Running `uv sync` for the `compile` extra should succeed, and remove the build dependencies.
|
||
uv_snapshot!(context.filters(), context.sync().arg("--extra").arg("compile"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved [N] packages in [TIME]
|
||
Prepared [N] packages in [TIME]
|
||
Uninstalled [N] packages in [TIME]
|
||
Installed [N] packages in [TIME]
|
||
- hatchling==1.22.4
|
||
- packaging==24.0
|
||
- pathspec==0.12.1
|
||
- pluggy==1.4.0
|
||
+ source-distribution==0.0.1 (from https://files.pythonhosted.org/packages/10/1f/57aa4cce1b1abf6b433106676e15f9fa2c92ed2bd4cf77c3b50a9e9ac773/source_distribution-0.0.1.tar.gz)
|
||
- trove-classifiers==2024.3.3
|
||
"###);
|
||
|
||
assert!(context.temp_dir.child("uv.lock").exists());
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Avoid using incompatible versions for build dependencies that are also part of the resolved
|
||
/// environment. This is a very subtle issue, but: when locking, we don't enforce platform
|
||
/// compatibility. So, if we reuse the resolver state to install, and the install itself has to
|
||
/// perform a resolution (e.g., for the build dependencies of a source distribution), that
|
||
/// resolution may choose incompatible versions.
|
||
///
|
||
/// The key property here is that there's a shared package between the build dependencies and the
|
||
/// project dependencies.
|
||
#[test]
|
||
fn sync_reset_state() -> 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 = ["pydantic-core"]
|
||
|
||
[build-system]
|
||
requires = ["setuptools", "pydantic-core"]
|
||
build-backend = "setuptools.build_meta:__legacy__"
|
||
"#,
|
||
)?;
|
||
|
||
let setup_py = context.temp_dir.child("setup.py");
|
||
setup_py.write_str(indoc::indoc! { r#"
|
||
from setuptools import setup
|
||
import pydantic_core
|
||
|
||
setup(
|
||
name="project",
|
||
version="0.1.0",
|
||
packages=["project"],
|
||
install_requires=["pydantic-core"],
|
||
)
|
||
"# })?;
|
||
|
||
let src = context.temp_dir.child("project");
|
||
src.create_dir_all()?;
|
||
|
||
let init = src.child("__init__.py");
|
||
init.touch()?;
|
||
|
||
// Running `uv sync` should succeed.
|
||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 3 packages in [TIME]
|
||
Prepared 3 packages in [TIME]
|
||
Installed 3 packages in [TIME]
|
||
+ project==0.1.0 (from file://[TEMP_DIR]/)
|
||
+ pydantic-core==2.17.0
|
||
+ typing-extensions==4.10.0
|
||
"###);
|
||
|
||
assert!(context.temp_dir.child("uv.lock").exists());
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Test that relative wheel paths are correctly preserved.
|
||
#[test]
|
||
fn sync_relative_wheel() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
let requirements = r#"[project]
|
||
name = "relative_wheel"
|
||
version = "0.1.0"
|
||
requires-python = ">=3.12"
|
||
dependencies = ["ok"]
|
||
|
||
[tool.uv.sources]
|
||
ok = { path = "wheels/ok-1.0.0-py3-none-any.whl" }
|
||
|
||
[build-system]
|
||
requires = ["hatchling"]
|
||
build-backend = "hatchling.build"
|
||
"#;
|
||
|
||
context
|
||
.temp_dir
|
||
.child("src/relative_wheel/__init__.py")
|
||
.touch()?;
|
||
|
||
context
|
||
.temp_dir
|
||
.child("pyproject.toml")
|
||
.write_str(requirements)?;
|
||
|
||
context.temp_dir.child("wheels").create_dir_all()?;
|
||
fs_err::copy(
|
||
"../../scripts/links/ok-1.0.0-py3-none-any.whl",
|
||
context.temp_dir.join("wheels/ok-1.0.0-py3-none-any.whl"),
|
||
)?;
|
||
|
||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 2 packages in [TIME]
|
||
Prepared 1 package in [TIME]
|
||
Installed 2 packages in [TIME]
|
||
+ ok==1.0.0 (from file://[TEMP_DIR]/wheels/ok-1.0.0-py3-none-any.whl)
|
||
+ relative-wheel==0.1.0 (from file://[TEMP_DIR]/)
|
||
"###);
|
||
|
||
let lock = context.read("uv.lock");
|
||
|
||
insta::with_settings!(
|
||
{
|
||
filters => context.filters(),
|
||
},
|
||
{
|
||
assert_snapshot!(
|
||
lock, @r###"
|
||
version = 1
|
||
requires-python = ">=3.12"
|
||
|
||
[options]
|
||
exclude-newer = "2024-03-25T00:00:00Z"
|
||
|
||
[[package]]
|
||
name = "ok"
|
||
version = "1.0.0"
|
||
source = { path = "wheels/ok-1.0.0-py3-none-any.whl" }
|
||
wheels = [
|
||
{ filename = "ok-1.0.0-py3-none-any.whl", hash = "sha256:79f0b33e6ce1e09eaa1784c8eee275dfe84d215d9c65c652f07c18e85fdaac5f" },
|
||
]
|
||
|
||
[[package]]
|
||
name = "relative-wheel"
|
||
version = "0.1.0"
|
||
source = { editable = "." }
|
||
dependencies = [
|
||
{ name = "ok" },
|
||
]
|
||
|
||
[package.metadata]
|
||
requires-dist = [{ name = "ok", path = "wheels/ok-1.0.0-py3-none-any.whl" }]
|
||
"###
|
||
);
|
||
}
|
||
);
|
||
|
||
// Check that we can re-read the lockfile.
|
||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 2 packages in [TIME]
|
||
Audited 2 packages in [TIME]
|
||
"###);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Syncing against an unstable environment should fail (but locking should succeed).
|
||
#[test]
|
||
fn sync_environment() -> 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.10"
|
||
dependencies = ["iniconfig"]
|
||
|
||
[tool.uv]
|
||
environments = ["python_version < '3.11'"]
|
||
"#,
|
||
)?;
|
||
|
||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 2 packages in [TIME]
|
||
error: The current Python platform is not compatible with the lockfile's supported environments: `python_full_version < '3.11'`
|
||
"###);
|
||
|
||
assert!(context.temp_dir.child("uv.lock").exists());
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn sync_dev() -> 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"]
|
||
|
||
[tool.uv]
|
||
dev-dependencies = ["anyio"]
|
||
"#,
|
||
)?;
|
||
|
||
context.lock().assert().success();
|
||
|
||
uv_snapshot!(context.filters(), context.sync().arg("--only-dev"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 5 packages in [TIME]
|
||
Prepared 3 packages in [TIME]
|
||
Installed 3 packages in [TIME]
|
||
+ anyio==4.3.0
|
||
+ idna==3.6
|
||
+ sniffio==1.3.1
|
||
"###);
|
||
|
||
uv_snapshot!(context.filters(), context.sync().arg("--no-dev"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 5 packages in [TIME]
|
||
Prepared 1 package in [TIME]
|
||
Uninstalled 3 packages in [TIME]
|
||
Installed 1 package in [TIME]
|
||
- anyio==4.3.0
|
||
- idna==3.6
|
||
- sniffio==1.3.1
|
||
+ typing-extensions==4.10.0
|
||
"###);
|
||
|
||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 5 packages in [TIME]
|
||
Installed 3 packages in [TIME]
|
||
+ anyio==4.3.0
|
||
+ idna==3.6
|
||
+ sniffio==1.3.1
|
||
"###);
|
||
|
||
// Using `--no-default-groups` should remove dev dependencies
|
||
uv_snapshot!(context.filters(), context.sync().arg("--no-default-groups"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 5 packages in [TIME]
|
||
Uninstalled 3 packages in [TIME]
|
||
- anyio==4.3.0
|
||
- idna==3.6
|
||
- sniffio==1.3.1
|
||
"###);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn sync_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"]
|
||
|
||
[tool.uv]
|
||
|
||
[dependency-groups]
|
||
dev = ["iniconfig"]
|
||
foo = ["anyio"]
|
||
bar = ["requests"]
|
||
"#,
|
||
)?;
|
||
|
||
context.lock().assert().success();
|
||
|
||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 10 packages in [TIME]
|
||
Prepared 2 packages in [TIME]
|
||
Installed 2 packages in [TIME]
|
||
+ iniconfig==2.0.0
|
||
+ typing-extensions==4.10.0
|
||
"###);
|
||
|
||
uv_snapshot!(context.filters(), context.sync().arg("--group").arg("foo"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 10 packages in [TIME]
|
||
Prepared 3 packages in [TIME]
|
||
Installed 3 packages in [TIME]
|
||
+ anyio==4.3.0
|
||
+ idna==3.6
|
||
+ sniffio==1.3.1
|
||
"###);
|
||
|
||
uv_snapshot!(context.filters(), context.sync().arg("--only-group").arg("bar"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 10 packages in [TIME]
|
||
Prepared 4 packages in [TIME]
|
||
Uninstalled 4 packages in [TIME]
|
||
Installed 4 packages in [TIME]
|
||
- anyio==4.3.0
|
||
+ certifi==2024.2.2
|
||
+ charset-normalizer==3.3.2
|
||
- iniconfig==2.0.0
|
||
+ requests==2.31.0
|
||
- sniffio==1.3.1
|
||
- typing-extensions==4.10.0
|
||
+ urllib3==2.2.1
|
||
"###);
|
||
|
||
uv_snapshot!(context.filters(), context.sync().arg("--group").arg("foo").arg("--group").arg("bar"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 10 packages in [TIME]
|
||
Installed 4 packages in [TIME]
|
||
+ anyio==4.3.0
|
||
+ iniconfig==2.0.0
|
||
+ sniffio==1.3.1
|
||
+ typing-extensions==4.10.0
|
||
"###);
|
||
|
||
uv_snapshot!(context.filters(), context.sync().arg("--all-groups"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 10 packages in [TIME]
|
||
Audited 9 packages in [TIME]
|
||
"###);
|
||
|
||
uv_snapshot!(context.filters(), context.sync().arg("--all-groups").arg("--no-group").arg("bar"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 10 packages in [TIME]
|
||
Uninstalled 4 packages in [TIME]
|
||
- certifi==2024.2.2
|
||
- charset-normalizer==3.3.2
|
||
- requests==2.31.0
|
||
- urllib3==2.2.1
|
||
"###);
|
||
|
||
uv_snapshot!(context.filters(), context.sync().arg("--all-groups").arg("--no-dev"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 10 packages in [TIME]
|
||
Uninstalled 1 package in [TIME]
|
||
Installed 4 packages in [TIME]
|
||
+ certifi==2024.2.2
|
||
+ charset-normalizer==3.3.2
|
||
- iniconfig==2.0.0
|
||
+ requests==2.31.0
|
||
+ urllib3==2.2.1
|
||
"###);
|
||
|
||
uv_snapshot!(context.filters(), context.sync().arg("--dev"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 10 packages in [TIME]
|
||
Uninstalled 7 packages in [TIME]
|
||
Installed 1 package in [TIME]
|
||
- anyio==4.3.0
|
||
- certifi==2024.2.2
|
||
- charset-normalizer==3.3.2
|
||
- idna==3.6
|
||
+ iniconfig==2.0.0
|
||
- requests==2.31.0
|
||
- sniffio==1.3.1
|
||
- urllib3==2.2.1
|
||
"###);
|
||
|
||
uv_snapshot!(context.filters(), context.sync().arg("--dev").arg("--no-group").arg("dev"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 10 packages in [TIME]
|
||
Uninstalled 1 package in [TIME]
|
||
- iniconfig==2.0.0
|
||
"###);
|
||
|
||
uv_snapshot!(context.filters(), context.sync().arg("--group").arg("dev").arg("--no-dev"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 10 packages in [TIME]
|
||
Audited 1 package in [TIME]
|
||
"###);
|
||
|
||
uv_snapshot!(context.filters(), context.sync().arg("--all-groups"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 10 packages in [TIME]
|
||
Installed 8 packages in [TIME]
|
||
+ anyio==4.3.0
|
||
+ certifi==2024.2.2
|
||
+ charset-normalizer==3.3.2
|
||
+ idna==3.6
|
||
+ iniconfig==2.0.0
|
||
+ requests==2.31.0
|
||
+ sniffio==1.3.1
|
||
+ urllib3==2.2.1
|
||
"###);
|
||
|
||
// Using `--no-default-groups` should exclude all groups
|
||
uv_snapshot!(context.filters(), context.sync().arg("--no-default-groups"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 10 packages in [TIME]
|
||
Uninstalled 8 packages in [TIME]
|
||
- anyio==4.3.0
|
||
- certifi==2024.2.2
|
||
- charset-normalizer==3.3.2
|
||
- idna==3.6
|
||
- iniconfig==2.0.0
|
||
- requests==2.31.0
|
||
- sniffio==1.3.1
|
||
- urllib3==2.2.1
|
||
"###);
|
||
|
||
uv_snapshot!(context.filters(), context.sync().arg("--all-groups"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 10 packages in [TIME]
|
||
Installed 8 packages in [TIME]
|
||
+ anyio==4.3.0
|
||
+ certifi==2024.2.2
|
||
+ charset-normalizer==3.3.2
|
||
+ idna==3.6
|
||
+ iniconfig==2.0.0
|
||
+ requests==2.31.0
|
||
+ sniffio==1.3.1
|
||
+ urllib3==2.2.1
|
||
"###);
|
||
|
||
// Using `--no-default-groups` with `--group foo` and `--group bar` should include those groups,
|
||
// excluding the remaining `dev` group.
|
||
uv_snapshot!(context.filters(), context.sync().arg("--no-default-groups").arg("--group").arg("foo").arg("--group").arg("bar"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 10 packages in [TIME]
|
||
Uninstalled 1 package in [TIME]
|
||
- iniconfig==2.0.0
|
||
"###);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn sync_include_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", {include-group = "bar"}]
|
||
bar = ["iniconfig"]
|
||
"#,
|
||
)?;
|
||
|
||
context.lock().assert().success();
|
||
|
||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 6 packages in [TIME]
|
||
Prepared 1 package in [TIME]
|
||
Installed 1 package in [TIME]
|
||
+ typing-extensions==4.10.0
|
||
"###);
|
||
|
||
uv_snapshot!(context.filters(), context.sync().arg("--group").arg("foo"), @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
|
||
+ idna==3.6
|
||
+ iniconfig==2.0.0
|
||
+ sniffio==1.3.1
|
||
"###);
|
||
|
||
uv_snapshot!(context.filters(), context.sync().arg("--only-group").arg("bar"), @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.sync().arg("--group").arg("foo").arg("--group").arg("bar"), @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
|
||
"###);
|
||
|
||
uv_snapshot!(context.filters(), context.sync().arg("--only-group").arg("foo"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 6 packages in [TIME]
|
||
Uninstalled 1 package in [TIME]
|
||
- typing-extensions==4.10.0
|
||
"###);
|
||
|
||
uv_snapshot!(context.filters(), context.sync().arg("--all-groups"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 6 packages in [TIME]
|
||
Installed 1 package in [TIME]
|
||
+ typing-extensions==4.10.0
|
||
"###);
|
||
|
||
uv_snapshot!(context.filters(), context.sync().arg("--no-default-groups"), @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.sync().arg("--all-groups"), @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
|
||
"###);
|
||
|
||
uv_snapshot!(context.filters(), context.sync().arg("--no-default-groups").arg("--group").arg("foo"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 6 packages in [TIME]
|
||
Audited 5 packages in [TIME]
|
||
"###);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn sync_exclude_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", {include-group = "bar"}]
|
||
bar = ["iniconfig"]
|
||
"#,
|
||
)?;
|
||
|
||
context.lock().assert().success();
|
||
|
||
uv_snapshot!(context.filters(), context.sync().arg("--group").arg("foo"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 6 packages in [TIME]
|
||
Prepared 5 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
|
||
"###);
|
||
|
||
uv_snapshot!(context.filters(), context.sync().arg("--group").arg("foo").arg("--no-group").arg("foo"), @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.sync().arg("--only-group").arg("bar"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 6 packages in [TIME]
|
||
Uninstalled 1 package in [TIME]
|
||
Installed 1 package in [TIME]
|
||
+ iniconfig==2.0.0
|
||
- typing-extensions==4.10.0
|
||
"###);
|
||
|
||
uv_snapshot!(context.filters(), context.sync().arg("--only-group").arg("bar").arg("--no-group").arg("bar"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 6 packages in [TIME]
|
||
Uninstalled 1 package in [TIME]
|
||
- iniconfig==2.0.0
|
||
"###);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn sync_dev_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"]
|
||
|
||
[tool.uv]
|
||
dev-dependencies = ["anyio"]
|
||
|
||
[dependency-groups]
|
||
dev = ["iniconfig"]
|
||
"#,
|
||
)?;
|
||
|
||
context.lock().assert().success();
|
||
|
||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 6 packages in [TIME]
|
||
Prepared 5 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
|
||
"###);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn sync_non_existent_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 = []
|
||
bar = ["requests"]
|
||
"#,
|
||
)?;
|
||
|
||
context.lock().assert().success();
|
||
|
||
// Requesting a non-existent group should fail.
|
||
uv_snapshot!(context.filters(), context.sync().arg("--group").arg("baz"), @r###"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: Group `baz` is not defined in the project's `dependency-group` table
|
||
"###);
|
||
|
||
uv_snapshot!(context.filters(), context.sync().arg("--no-group").arg("baz"), @r###"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: Group `baz` is not defined in the project's `dependency-group` table
|
||
"###);
|
||
|
||
// Requesting an empty group should succeed.
|
||
uv_snapshot!(context.filters(), context.sync().arg("--group").arg("foo"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 7 packages in [TIME]
|
||
Prepared 1 package in [TIME]
|
||
Installed 1 package in [TIME]
|
||
+ typing-extensions==4.10.0
|
||
"###);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn sync_non_existent_default_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 = []
|
||
|
||
[tool.uv]
|
||
default-groups = ["bar"]
|
||
"#,
|
||
)?;
|
||
|
||
context.lock().assert().success();
|
||
|
||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: Default group `bar` (from `tool.uv.default-groups`) is not defined in the project's `dependency-group` table
|
||
"###);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn sync_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]
|
||
dev = ["iniconfig"]
|
||
foo = ["anyio"]
|
||
bar = ["requests"]
|
||
"#,
|
||
)?;
|
||
|
||
context.lock().assert().success();
|
||
|
||
// The `dev` group should be synced by default.
|
||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 10 packages in [TIME]
|
||
Prepared 2 packages in [TIME]
|
||
Installed 2 packages in [TIME]
|
||
+ iniconfig==2.0.0
|
||
+ typing-extensions==4.10.0
|
||
"###);
|
||
|
||
// If we remove it from the `default-groups` list, it should be removed.
|
||
pyproject_toml.write_str(
|
||
r#"
|
||
[project]
|
||
name = "project"
|
||
version = "0.1.0"
|
||
requires-python = ">=3.12"
|
||
dependencies = ["typing-extensions"]
|
||
|
||
[dependency-groups]
|
||
dev = ["iniconfig"]
|
||
foo = ["anyio"]
|
||
bar = ["requests"]
|
||
|
||
[tool.uv]
|
||
default-groups = []
|
||
"#,
|
||
)?;
|
||
|
||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 10 packages in [TIME]
|
||
Uninstalled 1 package in [TIME]
|
||
- iniconfig==2.0.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]
|
||
dev = ["iniconfig"]
|
||
foo = ["anyio"]
|
||
bar = ["requests"]
|
||
|
||
[tool.uv]
|
||
default-groups = ["foo"]
|
||
"#,
|
||
)?;
|
||
|
||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 10 packages in [TIME]
|
||
Prepared 3 packages in [TIME]
|
||
Installed 3 packages in [TIME]
|
||
+ anyio==4.3.0
|
||
+ idna==3.6
|
||
+ sniffio==1.3.1
|
||
"###);
|
||
|
||
// `--no-group` should remove from the defaults.
|
||
pyproject_toml.write_str(
|
||
r#"
|
||
[project]
|
||
name = "project"
|
||
version = "0.1.0"
|
||
requires-python = ">=3.12"
|
||
dependencies = ["typing-extensions"]
|
||
|
||
[dependency-groups]
|
||
dev = ["iniconfig"]
|
||
foo = ["anyio"]
|
||
bar = ["requests"]
|
||
|
||
[tool.uv]
|
||
default-groups = ["foo"]
|
||
"#,
|
||
)?;
|
||
|
||
uv_snapshot!(context.filters(), context.sync().arg("--no-group").arg("foo"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 10 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.sync().arg("--group").arg("dev"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 10 packages 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.sync().arg("--all-groups"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 10 packages in [TIME]
|
||
Prepared 4 packages in [TIME]
|
||
Installed 4 packages in [TIME]
|
||
+ certifi==2024.2.2
|
||
+ charset-normalizer==3.3.2
|
||
+ requests==2.31.0
|
||
+ urllib3==2.2.1
|
||
"###);
|
||
|
||
// Using `--only-group` should exclude the defaults
|
||
uv_snapshot!(context.filters(), context.sync().arg("--only-group").arg("dev"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 10 packages in [TIME]
|
||
Uninstalled 8 packages in [TIME]
|
||
- anyio==4.3.0
|
||
- certifi==2024.2.2
|
||
- charset-normalizer==3.3.2
|
||
- idna==3.6
|
||
- requests==2.31.0
|
||
- sniffio==1.3.1
|
||
- typing-extensions==4.10.0
|
||
- urllib3==2.2.1
|
||
"###);
|
||
|
||
uv_snapshot!(context.filters(), context.sync().arg("--all-groups"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 10 packages in [TIME]
|
||
Installed 8 packages in [TIME]
|
||
+ anyio==4.3.0
|
||
+ certifi==2024.2.2
|
||
+ charset-normalizer==3.3.2
|
||
+ idna==3.6
|
||
+ requests==2.31.0
|
||
+ sniffio==1.3.1
|
||
+ typing-extensions==4.10.0
|
||
+ urllib3==2.2.1
|
||
"###);
|
||
|
||
// Using `--no-default-groups` should exclude all groups
|
||
uv_snapshot!(context.filters(), context.sync().arg("--no-default-groups"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 10 packages in [TIME]
|
||
Uninstalled 8 packages in [TIME]
|
||
- anyio==4.3.0
|
||
- certifi==2024.2.2
|
||
- charset-normalizer==3.3.2
|
||
- idna==3.6
|
||
- iniconfig==2.0.0
|
||
- requests==2.31.0
|
||
- sniffio==1.3.1
|
||
- urllib3==2.2.1
|
||
"###);
|
||
|
||
uv_snapshot!(context.filters(), context.sync().arg("--all-groups"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 10 packages in [TIME]
|
||
Installed 8 packages in [TIME]
|
||
+ anyio==4.3.0
|
||
+ certifi==2024.2.2
|
||
+ charset-normalizer==3.3.2
|
||
+ idna==3.6
|
||
+ iniconfig==2.0.0
|
||
+ requests==2.31.0
|
||
+ sniffio==1.3.1
|
||
+ urllib3==2.2.1
|
||
"###);
|
||
|
||
// Using `--no-default-groups` with `--group foo` and `--group bar` should include those groups,
|
||
// excluding the remaining `dev` group.
|
||
uv_snapshot!(context.filters(), context.sync().arg("--no-default-groups").arg("--group").arg("foo").arg("--group").arg("bar"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 10 packages in [TIME]
|
||
Uninstalled 1 package in [TIME]
|
||
- iniconfig==2.0.0
|
||
"###);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Sync with `--only-group`, where the group includes a workspace member.
|
||
#[test]
|
||
fn sync_group_member() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
// Create a workspace.
|
||
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 = ["iniconfig>=2"]
|
||
|
||
[dependency-groups]
|
||
foo = ["child", "typing-extensions>=4"]
|
||
|
||
[tool.uv.workspace]
|
||
members = ["child"]
|
||
|
||
[tool.uv.sources]
|
||
child = { workspace = true }
|
||
"#,
|
||
)?;
|
||
|
||
// Add a workspace member.
|
||
context
|
||
.temp_dir
|
||
.child("child")
|
||
.child("pyproject.toml")
|
||
.write_str(
|
||
r#"
|
||
[project]
|
||
name = "child"
|
||
version = "0.1.0"
|
||
requires-python = ">=3.12"
|
||
dependencies = ["iniconfig>=1"]
|
||
|
||
[build-system]
|
||
requires = ["setuptools>=42"]
|
||
build-backend = "setuptools.build_meta"
|
||
"#,
|
||
)?;
|
||
|
||
// Generate a lockfile.
|
||
context.lock().assert().success();
|
||
|
||
uv_snapshot!(context.filters(), context.sync().arg("--only-group").arg("foo"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 4 packages in [TIME]
|
||
Prepared 3 packages in [TIME]
|
||
Installed 3 packages in [TIME]
|
||
+ child==0.1.0 (from file://[TEMP_DIR]/child)
|
||
+ iniconfig==2.0.0
|
||
+ typing-extensions==4.10.0
|
||
"###);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Sync with `--only-group`, where the group includes a legacy non-`[project]` workspace member.
|
||
#[test]
|
||
fn sync_group_legacy_non_project_member() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
// Create a workspace.
|
||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||
pyproject_toml.write_str(
|
||
r#"
|
||
[dependency-groups]
|
||
foo = ["child", "typing-extensions>=4"]
|
||
|
||
[tool.uv.workspace]
|
||
members = ["child"]
|
||
|
||
[tool.uv.sources]
|
||
child = { workspace = true }
|
||
"#,
|
||
)?;
|
||
|
||
// Add a workspace member.
|
||
context
|
||
.temp_dir
|
||
.child("child")
|
||
.child("pyproject.toml")
|
||
.write_str(
|
||
r#"
|
||
[project]
|
||
name = "child"
|
||
version = "0.1.0"
|
||
requires-python = ">=3.12"
|
||
dependencies = ["iniconfig>=1"]
|
||
|
||
[build-system]
|
||
requires = ["setuptools>=42"]
|
||
build-backend = "setuptools.build_meta"
|
||
"#,
|
||
)?;
|
||
|
||
// Generate a lockfile.
|
||
uv_snapshot!(context.filters(), context.lock(), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 3 packages in [TIME]
|
||
"###);
|
||
|
||
let lock = context.read("uv.lock");
|
||
|
||
insta::with_settings!({
|
||
filters => context.filters(),
|
||
}, {
|
||
assert_snapshot!(
|
||
lock, @r###"
|
||
version = 1
|
||
requires-python = ">=3.12"
|
||
|
||
[options]
|
||
exclude-newer = "2024-03-25T00:00:00Z"
|
||
|
||
[manifest]
|
||
members = [
|
||
"child",
|
||
]
|
||
|
||
[manifest.dependency-groups]
|
||
foo = [
|
||
{ name = "child", editable = "child" },
|
||
{ name = "typing-extensions", specifier = ">=4" },
|
||
]
|
||
|
||
[[package]]
|
||
name = "child"
|
||
version = "0.1.0"
|
||
source = { editable = "child" }
|
||
dependencies = [
|
||
{ name = "iniconfig" },
|
||
]
|
||
|
||
[package.metadata]
|
||
requires-dist = [{ name = "iniconfig", specifier = ">=1" }]
|
||
|
||
[[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 }
|
||
wheels = [
|
||
{ url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 },
|
||
]
|
||
|
||
[[package]]
|
||
name = "typing-extensions"
|
||
version = "4.10.0"
|
||
source = { registry = "https://pypi.org/simple" }
|
||
sdist = { url = "https://files.pythonhosted.org/packages/16/3a/0d26ce356c7465a19c9ea8814b960f8a36c3b0d07c323176620b7b483e44/typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb", size = 77558 }
|
||
wheels = [
|
||
{ url = "https://files.pythonhosted.org/packages/f9/de/dc04a3ea60b22624b51c703a84bbe0184abcd1d0b9bc8074b5d6b7ab90bb/typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475", size = 33926 },
|
||
]
|
||
"###
|
||
);
|
||
});
|
||
|
||
uv_snapshot!(context.filters(), context.sync().arg("--only-group").arg("foo"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 3 packages in [TIME]
|
||
Prepared 3 packages in [TIME]
|
||
Installed 3 packages in [TIME]
|
||
+ child==0.1.0 (from file://[TEMP_DIR]/child)
|
||
+ iniconfig==2.0.0
|
||
+ typing-extensions==4.10.0
|
||
"###);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Sync with `--only-group`, where the group includes the project itself.
|
||
#[test]
|
||
fn sync_group_self() -> 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 = ["iniconfig>=2"]
|
||
|
||
[project.optional-dependencies]
|
||
test = ["idna>=3"]
|
||
|
||
[build-system]
|
||
requires = ["setuptools>=42"]
|
||
build-backend = "setuptools.build_meta"
|
||
|
||
[dependency-groups]
|
||
foo = ["project", "typing-extensions>=4"]
|
||
bar = ["project[test]"]
|
||
"#,
|
||
)?;
|
||
|
||
// Generate a lockfile.
|
||
uv_snapshot!(context.filters(), context.lock(), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 4 packages in [TIME]
|
||
"###);
|
||
|
||
let lock = context.read("uv.lock");
|
||
|
||
insta::with_settings!({
|
||
filters => context.filters(),
|
||
}, {
|
||
assert_snapshot!(
|
||
lock, @r###"
|
||
version = 1
|
||
requires-python = ">=3.12"
|
||
|
||
[options]
|
||
exclude-newer = "2024-03-25T00:00:00Z"
|
||
|
||
[[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 }
|
||
wheels = [
|
||
{ url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567 },
|
||
]
|
||
|
||
[[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 }
|
||
wheels = [
|
||
{ url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 },
|
||
]
|
||
|
||
[[package]]
|
||
name = "project"
|
||
version = "0.1.0"
|
||
source = { editable = "." }
|
||
dependencies = [
|
||
{ name = "iniconfig" },
|
||
]
|
||
|
||
[package.optional-dependencies]
|
||
test = [
|
||
{ name = "idna" },
|
||
]
|
||
|
||
[package.dev-dependencies]
|
||
bar = [
|
||
{ name = "project", extra = ["test"] },
|
||
]
|
||
foo = [
|
||
{ name = "project" },
|
||
{ name = "typing-extensions" },
|
||
]
|
||
|
||
[package.metadata]
|
||
requires-dist = [
|
||
{ name = "idna", marker = "extra == 'test'", specifier = ">=3" },
|
||
{ name = "iniconfig", specifier = ">=2" },
|
||
]
|
||
|
||
[package.metadata.requires-dev]
|
||
bar = [{ name = "project", extras = ["test"] }]
|
||
foo = [
|
||
{ name = "project" },
|
||
{ name = "typing-extensions", specifier = ">=4" },
|
||
]
|
||
|
||
[[package]]
|
||
name = "typing-extensions"
|
||
version = "4.10.0"
|
||
source = { registry = "https://pypi.org/simple" }
|
||
sdist = { url = "https://files.pythonhosted.org/packages/16/3a/0d26ce356c7465a19c9ea8814b960f8a36c3b0d07c323176620b7b483e44/typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb", size = 77558 }
|
||
wheels = [
|
||
{ url = "https://files.pythonhosted.org/packages/f9/de/dc04a3ea60b22624b51c703a84bbe0184abcd1d0b9bc8074b5d6b7ab90bb/typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475", size = 33926 },
|
||
]
|
||
"###
|
||
);
|
||
});
|
||
|
||
uv_snapshot!(context.filters(), context.sync().arg("--only-group").arg("foo"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 4 packages in [TIME]
|
||
Prepared 3 packages in [TIME]
|
||
Installed 3 packages in [TIME]
|
||
+ iniconfig==2.0.0
|
||
+ project==0.1.0 (from file://[TEMP_DIR]/)
|
||
+ typing-extensions==4.10.0
|
||
"###);
|
||
|
||
uv_snapshot!(context.filters(), context.sync().arg("--only-group").arg("bar"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 4 packages in [TIME]
|
||
Prepared 1 package in [TIME]
|
||
Uninstalled 1 package in [TIME]
|
||
Installed 1 package in [TIME]
|
||
+ idna==3.6
|
||
- typing-extensions==4.10.0
|
||
"###);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Regression test for <https://github.com/astral-sh/uv/issues/6316>.
|
||
///
|
||
/// Previously, we would read metadata statically from pyproject.toml and write that to `uv.lock`. In
|
||
/// this sync pass, we had also built the project with setuptools, which sorts specifiers by python
|
||
/// string sort through packaging. On the second run, we read the cache that now has the setuptools
|
||
/// sorting, changing the lockfile.
|
||
#[test]
|
||
fn read_metadata_statically_over_the_cache() -> 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"
|
||
# Python string sorting is the other way round.
|
||
dependencies = ["anyio>=4,<5"]
|
||
|
||
[build-system]
|
||
requires = ["setuptools>=42"]
|
||
build-backend = "setuptools.build_meta"
|
||
"#,
|
||
)?;
|
||
|
||
context.sync().assert().success();
|
||
let lock1 = context.read("uv.lock");
|
||
// Assert we're reading static metadata.
|
||
assert!(lock1.contains(">=4,<5"));
|
||
assert!(!lock1.contains("<5,>=4"));
|
||
context.sync().assert().success();
|
||
let lock2 = context.read("uv.lock");
|
||
// Assert stability.
|
||
assert_eq!(lock1, lock2);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Avoid syncing the project package when `--no-install-project` is provided.
|
||
#[test]
|
||
fn no_install_project() -> 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"
|
||
"#,
|
||
)?;
|
||
|
||
// Generate a lockfile.
|
||
context.lock().assert().success();
|
||
|
||
// Running with `--no-install-project` should install `anyio`, but not `project`.
|
||
uv_snapshot!(context.filters(), context.sync().arg("--no-install-project"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 4 packages in [TIME]
|
||
Prepared 3 packages in [TIME]
|
||
Installed 3 packages in [TIME]
|
||
+ anyio==3.7.0
|
||
+ idna==3.6
|
||
+ sniffio==1.3.1
|
||
"###);
|
||
|
||
// However, we do require the `pyproject.toml`.
|
||
fs_err::remove_file(pyproject_toml)?;
|
||
|
||
uv_snapshot!(context.filters(), context.sync().arg("--no-install-project"), @r###"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: No `pyproject.toml` found in current directory or any parent directory
|
||
"###);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Avoid syncing workspace members and the project when `--no-install-workspace` is provided, but
|
||
/// include all dependencies.
|
||
#[test]
|
||
fn no_install_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.7.0", "child"]
|
||
|
||
[build-system]
|
||
requires = ["setuptools>=42"]
|
||
build-backend = "setuptools.build_meta"
|
||
|
||
[tool.uv.workspace]
|
||
members = ["child"]
|
||
|
||
[tool.uv.sources]
|
||
child = { workspace = true }
|
||
"#,
|
||
)?;
|
||
|
||
// Add a workspace member.
|
||
let child = context.temp_dir.child("child");
|
||
child.child("pyproject.toml").write_str(
|
||
r#"
|
||
[project]
|
||
name = "child"
|
||
version = "0.1.0"
|
||
requires-python = ">=3.12"
|
||
dependencies = ["iniconfig>=1"]
|
||
|
||
[build-system]
|
||
requires = ["setuptools>=42"]
|
||
build-backend = "setuptools.build_meta"
|
||
"#,
|
||
)?;
|
||
child
|
||
.child("src")
|
||
.child("child")
|
||
.child("__init__.py")
|
||
.touch()?;
|
||
|
||
// Generate a lockfile.
|
||
context.lock().assert().success();
|
||
|
||
// Running with `--no-install-workspace` should install `anyio` and `iniconfig`, but not
|
||
// `project` or `child`.
|
||
uv_snapshot!(context.filters(), context.sync().arg("--no-install-workspace"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 6 packages in [TIME]
|
||
Prepared 4 packages in [TIME]
|
||
Installed 4 packages in [TIME]
|
||
+ anyio==3.7.0
|
||
+ idna==3.6
|
||
+ iniconfig==2.0.0
|
||
+ sniffio==1.3.1
|
||
"###);
|
||
|
||
// Remove the virtual environment.
|
||
fs_err::remove_dir_all(&context.venv)?;
|
||
|
||
// We don't require the `pyproject.toml` for non-root members, if `--frozen` is provided.
|
||
fs_err::remove_file(child.join("pyproject.toml"))?;
|
||
|
||
uv_snapshot!(context.filters(), context.sync().arg("--no-install-workspace").arg("--frozen"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
|
||
Creating virtual environment at: .venv
|
||
Installed 4 packages in [TIME]
|
||
+ anyio==3.7.0
|
||
+ idna==3.6
|
||
+ iniconfig==2.0.0
|
||
+ sniffio==1.3.1
|
||
"###);
|
||
|
||
// Even if `--package` is used.
|
||
uv_snapshot!(context.filters(), context.sync().arg("--package").arg("child").arg("--no-install-workspace").arg("--frozen"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Uninstalled 3 packages in [TIME]
|
||
- anyio==3.7.0
|
||
- idna==3.6
|
||
- sniffio==1.3.1
|
||
"###);
|
||
|
||
// Unless the package doesn't exist.
|
||
uv_snapshot!(context.filters(), context.sync().arg("--package").arg("fake").arg("--no-install-workspace").arg("--frozen"), @r###"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: Could not find root package `fake`
|
||
"###);
|
||
|
||
// Even if `--all-packages` is used.
|
||
uv_snapshot!(context.filters(), context.sync().arg("--all-packages").arg("--no-install-workspace").arg("--frozen"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Installed 3 packages in [TIME]
|
||
+ anyio==3.7.0
|
||
+ idna==3.6
|
||
+ sniffio==1.3.1
|
||
"###);
|
||
|
||
// But we do require the root `pyproject.toml`.
|
||
fs_err::remove_file(context.temp_dir.join("pyproject.toml"))?;
|
||
|
||
uv_snapshot!(context.filters(), context.sync().arg("--no-install-workspace").arg("--frozen"), @r###"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: No `pyproject.toml` found in current directory or any parent directory
|
||
"###);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Avoid syncing the target package when `--no-install-package` is provided.
|
||
#[test]
|
||
fn no_install_package() -> 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"
|
||
"#,
|
||
)?;
|
||
|
||
// Generate a lockfile.
|
||
context.lock().assert().success();
|
||
|
||
// Running with `--no-install-package anyio` should skip anyio but include everything else
|
||
uv_snapshot!(context.filters(), context.sync().arg("--no-install-package").arg("anyio"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 4 packages in [TIME]
|
||
Prepared 3 packages in [TIME]
|
||
Installed 3 packages in [TIME]
|
||
+ idna==3.6
|
||
+ project==0.1.0 (from file://[TEMP_DIR]/)
|
||
+ sniffio==1.3.1
|
||
"###);
|
||
|
||
// Running with `--no-install-package project` should skip the project itself (not as a special
|
||
// case, that's just the name of the project)
|
||
uv_snapshot!(context.filters(), context.sync().arg("--no-install-package").arg("project"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 4 packages in [TIME]
|
||
Prepared 1 package in [TIME]
|
||
Uninstalled 1 package in [TIME]
|
||
Installed 1 package in [TIME]
|
||
+ anyio==3.7.0
|
||
- project==0.1.0 (from file://[TEMP_DIR]/)
|
||
"###);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Ensure that `--no-build` isn't enforced for projects that aren't installed in the first place.
|
||
#[test]
|
||
fn no_install_project_no_build() -> 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"
|
||
"#,
|
||
)?;
|
||
|
||
// Generate a lockfile.
|
||
context.lock().assert().success();
|
||
|
||
// `--no-build` should raise an error, since we try to install the project.
|
||
uv_snapshot!(context.filters(), context.sync().arg("--no-build"), @r###"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 4 packages in [TIME]
|
||
error: Distribution `project==0.1.0 @ editable+.` can't be installed because it is marked as `--no-build` but has no binary distribution
|
||
"###);
|
||
|
||
// But it's fine to combine `--no-install-project` with `--no-build`. We shouldn't error, since
|
||
// we aren't building the project.
|
||
uv_snapshot!(context.filters(), context.sync().arg("--no-install-project").arg("--no-build").arg("--locked"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 4 packages in [TIME]
|
||
Prepared 3 packages in [TIME]
|
||
Installed 3 packages in [TIME]
|
||
+ anyio==3.7.0
|
||
+ idna==3.6
|
||
+ sniffio==1.3.1
|
||
"###);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Convert from a package to a virtual project.
|
||
#[test]
|
||
fn convert_to_virtual() -> 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 = ["iniconfig"]
|
||
|
||
[build-system]
|
||
requires = ["setuptools>=42"]
|
||
build-backend = "setuptools.build_meta"
|
||
"#,
|
||
)?;
|
||
|
||
// Running `uv sync` should install the project itself.
|
||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 2 packages in [TIME]
|
||
Prepared 2 packages in [TIME]
|
||
Installed 2 packages in [TIME]
|
||
+ iniconfig==2.0.0
|
||
+ project==0.1.0 (from file://[TEMP_DIR]/)
|
||
"###);
|
||
|
||
let lock = context.read("uv.lock");
|
||
|
||
insta::with_settings!({
|
||
filters => context.filters(),
|
||
}, {
|
||
assert_snapshot!(
|
||
lock, @r###"
|
||
version = 1
|
||
requires-python = ">=3.12"
|
||
|
||
[options]
|
||
exclude-newer = "2024-03-25T00:00:00Z"
|
||
|
||
[[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 }
|
||
wheels = [
|
||
{ url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 },
|
||
]
|
||
|
||
[[package]]
|
||
name = "project"
|
||
version = "0.1.0"
|
||
source = { editable = "." }
|
||
dependencies = [
|
||
{ name = "iniconfig" },
|
||
]
|
||
|
||
[package.metadata]
|
||
requires-dist = [{ name = "iniconfig" }]
|
||
"###
|
||
);
|
||
});
|
||
|
||
// Remove the build system.
|
||
pyproject_toml.write_str(
|
||
r#"
|
||
[project]
|
||
name = "project"
|
||
version = "0.1.0"
|
||
requires-python = ">=3.12"
|
||
dependencies = ["iniconfig"]
|
||
"#,
|
||
)?;
|
||
|
||
// Running `uv sync` should remove the project itself.
|
||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 2 packages in [TIME]
|
||
Uninstalled 1 package in [TIME]
|
||
- project==0.1.0 (from file://[TEMP_DIR]/)
|
||
"###);
|
||
|
||
let lock = context.read("uv.lock");
|
||
|
||
insta::with_settings!({
|
||
filters => context.filters(),
|
||
}, {
|
||
assert_snapshot!(
|
||
lock, @r###"
|
||
version = 1
|
||
requires-python = ">=3.12"
|
||
|
||
[options]
|
||
exclude-newer = "2024-03-25T00:00:00Z"
|
||
|
||
[[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 }
|
||
wheels = [
|
||
{ url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 },
|
||
]
|
||
|
||
[[package]]
|
||
name = "project"
|
||
version = "0.1.0"
|
||
source = { virtual = "." }
|
||
dependencies = [
|
||
{ name = "iniconfig" },
|
||
]
|
||
|
||
[package.metadata]
|
||
requires-dist = [{ name = "iniconfig" }]
|
||
"###
|
||
);
|
||
});
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Convert from a virtual project to a package.
|
||
#[test]
|
||
fn convert_to_package() -> 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 = ["iniconfig"]
|
||
"#,
|
||
)?;
|
||
|
||
// Running `uv sync` should not install the project itself.
|
||
uv_snapshot!(context.filters(), context.sync(), @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
|
||
"###);
|
||
|
||
let lock = context.read("uv.lock");
|
||
|
||
insta::with_settings!({
|
||
filters => context.filters(),
|
||
}, {
|
||
assert_snapshot!(
|
||
lock, @r###"
|
||
version = 1
|
||
requires-python = ">=3.12"
|
||
|
||
[options]
|
||
exclude-newer = "2024-03-25T00:00:00Z"
|
||
|
||
[[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 }
|
||
wheels = [
|
||
{ url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 },
|
||
]
|
||
|
||
[[package]]
|
||
name = "project"
|
||
version = "0.1.0"
|
||
source = { virtual = "." }
|
||
dependencies = [
|
||
{ name = "iniconfig" },
|
||
]
|
||
|
||
[package.metadata]
|
||
requires-dist = [{ name = "iniconfig" }]
|
||
"###
|
||
);
|
||
});
|
||
|
||
// Add the build system.
|
||
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 `uv sync` should install the project itself.
|
||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 2 packages in [TIME]
|
||
Prepared 1 package in [TIME]
|
||
Installed 1 package in [TIME]
|
||
+ project==0.1.0 (from file://[TEMP_DIR]/)
|
||
"###);
|
||
|
||
let lock = context.read("uv.lock");
|
||
|
||
insta::with_settings!({
|
||
filters => context.filters(),
|
||
}, {
|
||
assert_snapshot!(
|
||
lock, @r###"
|
||
version = 1
|
||
requires-python = ">=3.12"
|
||
|
||
[options]
|
||
exclude-newer = "2024-03-25T00:00:00Z"
|
||
|
||
[[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 }
|
||
wheels = [
|
||
{ url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 },
|
||
]
|
||
|
||
[[package]]
|
||
name = "project"
|
||
version = "0.1.0"
|
||
source = { editable = "." }
|
||
dependencies = [
|
||
{ name = "iniconfig" },
|
||
]
|
||
|
||
[package.metadata]
|
||
requires-dist = [{ name = "iniconfig" }]
|
||
"###
|
||
);
|
||
});
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn sync_custom_environment_path() -> Result<()> {
|
||
let mut 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.12"
|
||
dependencies = ["iniconfig"]
|
||
"#,
|
||
)?;
|
||
|
||
// Running `uv sync` should create `.venv` by default
|
||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- 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
|
||
"###);
|
||
|
||
context
|
||
.temp_dir
|
||
.child(".venv")
|
||
.assert(predicate::path::is_dir());
|
||
|
||
// Running `uv sync` should create `foo` in the project directory when customized
|
||
uv_snapshot!(context.filters(), context.sync().env(EnvVars::UV_PROJECT_ENVIRONMENT, "foo"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
|
||
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());
|
||
|
||
// We don't delete `.venv`, though we arguably could
|
||
context
|
||
.temp_dir
|
||
.child(".venv")
|
||
.assert(predicate::path::is_dir());
|
||
|
||
// An absolute path can be provided
|
||
uv_snapshot!(context.filters(), context.sync().env(EnvVars::UV_PROJECT_ENVIRONMENT, "foobar/.venv"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
|
||
Creating virtual environment at: foobar/.venv
|
||
Resolved 2 packages in [TIME]
|
||
Installed 1 package in [TIME]
|
||
+ iniconfig==2.0.0
|
||
"###);
|
||
|
||
context
|
||
.temp_dir
|
||
.child("foobar")
|
||
.assert(predicate::path::is_dir());
|
||
|
||
context
|
||
.temp_dir
|
||
.child("foobar")
|
||
.child(".venv")
|
||
.assert(predicate::path::is_dir());
|
||
|
||
// An absolute path can be provided
|
||
uv_snapshot!(context.filters(), context.sync().env(EnvVars::UV_PROJECT_ENVIRONMENT, context.temp_dir.join("bar")), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
|
||
Creating virtual environment at: bar
|
||
Resolved 2 packages in [TIME]
|
||
Installed 1 package in [TIME]
|
||
+ iniconfig==2.0.0
|
||
"###);
|
||
|
||
context
|
||
.temp_dir
|
||
.child("bar")
|
||
.assert(predicate::path::is_dir());
|
||
|
||
// And, it can be outside the project
|
||
let tempdir = tempdir_in(TestContext::test_bucket_dir())?;
|
||
context = context.with_filtered_path(tempdir.path(), "OTHER_TEMPDIR");
|
||
uv_snapshot!(context.filters(), context.sync().env(EnvVars::UV_PROJECT_ENVIRONMENT, tempdir.path().join(".venv")), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
|
||
Creating virtual environment at: [OTHER_TEMPDIR]/.venv
|
||
Resolved 2 packages in [TIME]
|
||
Installed 1 package in [TIME]
|
||
+ iniconfig==2.0.0
|
||
"###);
|
||
|
||
ChildPath::new(tempdir.path())
|
||
.child(".venv")
|
||
.assert(predicate::path::is_dir());
|
||
|
||
// If the directory already exists and is not a virtual environment we should fail with an error
|
||
fs_err::remove_dir_all(context.temp_dir.join("foo"))?;
|
||
fs_err::create_dir(context.temp_dir.join("foo"))?;
|
||
fs_err::write(context.temp_dir.join("foo").join("file"), b"")?;
|
||
uv_snapshot!(context.filters(), context.sync().env(EnvVars::UV_PROJECT_ENVIRONMENT, "foo"), @r###"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: Project virtual environment directory `[TEMP_DIR]/foo` cannot be used because it is not a valid Python environment (no Python executable was found)
|
||
"###);
|
||
|
||
// But if it's just an incompatible virtual environment...
|
||
fs_err::remove_dir_all(context.temp_dir.join("foo"))?;
|
||
uv_snapshot!(context.filters(), context.venv().arg("foo").arg("--python").arg("3.11"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Using CPython 3.11.[X] interpreter at: [PYTHON-3.11]
|
||
warning: The requested interpreter resolved to Python 3.11.[X], which is incompatible with the project's Python requirement: `>=3.12`
|
||
Creating virtual environment at: foo
|
||
Activate with: source foo/[BIN]/activate
|
||
"###);
|
||
|
||
// Even with some extraneous content...
|
||
fs_err::write(context.temp_dir.join("foo").join("file"), b"")?;
|
||
|
||
// We can delete and use it
|
||
uv_snapshot!(context.filters(), context.sync().env(EnvVars::UV_PROJECT_ENVIRONMENT, "foo"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- 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 sync_workspace_custom_environment_path() -> 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 = ["iniconfig"]
|
||
"#,
|
||
)?;
|
||
|
||
// Create a workspace member
|
||
context.init().arg("child").assert().success();
|
||
|
||
// Running `uv sync` should create `.venv` in the workspace root
|
||
uv_snapshot!(context.filters(), context.sync(), @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
|
||
"###);
|
||
|
||
context
|
||
.temp_dir
|
||
.child(".venv")
|
||
.assert(predicate::path::is_dir());
|
||
|
||
// Similarly, `uv sync` from the child project uses `.venv` in the workspace root
|
||
uv_snapshot!(context.filters(), context.sync().current_dir(context.temp_dir.join("child")), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 3 packages in [TIME]
|
||
Uninstalled 1 package in [TIME]
|
||
- iniconfig==2.0.0
|
||
"###);
|
||
|
||
context
|
||
.temp_dir
|
||
.child(".venv")
|
||
.assert(predicate::path::is_dir());
|
||
|
||
context
|
||
.temp_dir
|
||
.child("child")
|
||
.child(".venv")
|
||
.assert(predicate::path::missing());
|
||
|
||
// Running `uv sync` should create `foo` in the workspace root when customized
|
||
uv_snapshot!(context.filters(), context.sync().env(EnvVars::UV_PROJECT_ENVIRONMENT, "foo"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
|
||
Creating virtual environment at: foo
|
||
Resolved 3 packages in [TIME]
|
||
Installed 1 package in [TIME]
|
||
+ iniconfig==2.0.0
|
||
"###);
|
||
|
||
context
|
||
.temp_dir
|
||
.child("foo")
|
||
.assert(predicate::path::is_dir());
|
||
|
||
// We don't delete `.venv`, though we arguably could
|
||
context
|
||
.temp_dir
|
||
.child(".venv")
|
||
.assert(predicate::path::is_dir());
|
||
|
||
// Similarly, `uv sync` from the child project uses `foo` relative to the workspace root
|
||
uv_snapshot!(context.filters(), context.sync().env(EnvVars::UV_PROJECT_ENVIRONMENT, "foo").current_dir(context.temp_dir.join("child")), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 3 packages in [TIME]
|
||
Uninstalled 1 package in [TIME]
|
||
- iniconfig==2.0.0
|
||
"###);
|
||
|
||
context
|
||
.temp_dir
|
||
.child("foo")
|
||
.assert(predicate::path::is_dir());
|
||
|
||
context
|
||
.temp_dir
|
||
.child("child")
|
||
.child("foo")
|
||
.assert(predicate::path::missing());
|
||
|
||
// And, `uv sync --package child` uses `foo` relative to the workspace root
|
||
uv_snapshot!(context.filters(), context.sync().arg("--package").arg("child").env(EnvVars::UV_PROJECT_ENVIRONMENT, "foo"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 3 packages in [TIME]
|
||
Audited in [TIME]
|
||
"###);
|
||
|
||
context
|
||
.temp_dir
|
||
.child("foo")
|
||
.assert(predicate::path::is_dir());
|
||
|
||
context
|
||
.temp_dir
|
||
.child("child")
|
||
.child("foo")
|
||
.assert(predicate::path::missing());
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn sync_empty_virtual_environment() -> Result<()> {
|
||
let context = TestContext::new_with_versions(&["3.12"]);
|
||
|
||
// Create an empty directory
|
||
context.temp_dir.child(".venv").create_dir_all()?;
|
||
|
||
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 = ["iniconfig"]
|
||
"#,
|
||
)?;
|
||
|
||
// Running `uv sync` should work
|
||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- 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
|
||
"###);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Test for warnings when `VIRTUAL_ENV` is set but will not be respected.
|
||
#[test]
|
||
fn sync_legacy_non_project_warning() -> 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 = ["iniconfig"]
|
||
"#,
|
||
)?;
|
||
|
||
// We should not warn if it matches the project environment
|
||
uv_snapshot!(context.filters(), context.sync().env(EnvVars::VIRTUAL_ENV, context.temp_dir.join(".venv")), @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
|
||
"###);
|
||
|
||
// Including if it's a relative path that matches
|
||
uv_snapshot!(context.filters(), context.sync().env(EnvVars::VIRTUAL_ENV, ".venv"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 2 packages in [TIME]
|
||
Audited 1 package in [TIME]
|
||
"###);
|
||
|
||
// Or, if it's a link that resolves to the same path
|
||
#[cfg(unix)]
|
||
{
|
||
use fs_err::os::unix::fs::symlink;
|
||
|
||
let link = context.temp_dir.join("link");
|
||
symlink(context.temp_dir.join(".venv"), &link)?;
|
||
|
||
uv_snapshot!(context.filters(), context.sync().env(EnvVars::VIRTUAL_ENV, link), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 2 packages in [TIME]
|
||
Audited 1 package in [TIME]
|
||
"###);
|
||
}
|
||
|
||
// But we should warn if it's a different path
|
||
uv_snapshot!(context.filters(), context.sync().env(EnvVars::VIRTUAL_ENV, "foo"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
warning: `VIRTUAL_ENV=foo` does not match the project environment path `.venv` and will be ignored
|
||
Resolved 2 packages in [TIME]
|
||
Audited 1 package in [TIME]
|
||
"###);
|
||
|
||
// Including absolute paths
|
||
uv_snapshot!(context.filters(), context.sync().env(EnvVars::VIRTUAL_ENV, context.temp_dir.join("foo")), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
warning: `VIRTUAL_ENV=foo` does not match the project environment path `.venv` and will be ignored
|
||
Resolved 2 packages in [TIME]
|
||
Audited 1 package in [TIME]
|
||
"###);
|
||
|
||
// We should not warn if the project environment has been customized and matches
|
||
uv_snapshot!(context.filters(), context.sync().env(EnvVars::VIRTUAL_ENV, "foo").env(EnvVars::UV_PROJECT_ENVIRONMENT, "foo"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
|
||
Creating virtual environment at: foo
|
||
Resolved 2 packages in [TIME]
|
||
Installed 1 package in [TIME]
|
||
+ iniconfig==2.0.0
|
||
"###);
|
||
|
||
// But we should warn if they don't match still
|
||
uv_snapshot!(context.filters(), context.sync().env(EnvVars::VIRTUAL_ENV, "foo").env(EnvVars::UV_PROJECT_ENVIRONMENT, "bar"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
warning: `VIRTUAL_ENV=foo` does not match the project environment path `bar` and will be ignored
|
||
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
|
||
Creating virtual environment at: bar
|
||
Resolved 2 packages in [TIME]
|
||
Installed 1 package in [TIME]
|
||
+ iniconfig==2.0.0
|
||
"###);
|
||
|
||
let child = context.temp_dir.child("child");
|
||
child.create_dir_all()?;
|
||
|
||
// And `VIRTUAL_ENV` is resolved relative to the project root so with relative paths we should
|
||
// warn from a child too
|
||
uv_snapshot!(context.filters(), context.sync().env(EnvVars::VIRTUAL_ENV, "foo").env(EnvVars::UV_PROJECT_ENVIRONMENT, "foo").current_dir(&child), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
warning: `VIRTUAL_ENV=foo` does not match the project environment path `[TEMP_DIR]/foo` and will be ignored
|
||
Resolved 2 packages in [TIME]
|
||
Audited 1 package in [TIME]
|
||
"###);
|
||
|
||
// But, a matching absolute path shouldn't warn
|
||
uv_snapshot!(context.filters(), context.sync().env(EnvVars::VIRTUAL_ENV, context.temp_dir.join("foo")).env(EnvVars::UV_PROJECT_ENVIRONMENT, "foo").current_dir(&child), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 2 packages in [TIME]
|
||
Audited 1 package in [TIME]
|
||
"###);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn sync_update_project() -> Result<()> {
|
||
let context = TestContext::new_with_versions(&["3.12"]);
|
||
|
||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||
pyproject_toml.write_str(
|
||
r#"
|
||
[project]
|
||
name = "my-project"
|
||
version = "0.1.0"
|
||
requires-python = ">=3.12"
|
||
dependencies = ["iniconfig"]
|
||
"#,
|
||
)?;
|
||
|
||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- 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
|
||
"###);
|
||
|
||
// Bump the project version.
|
||
pyproject_toml.write_str(
|
||
r#"
|
||
[project]
|
||
name = "my-project"
|
||
version = "0.2.0"
|
||
requires-python = ">=3.12"
|
||
dependencies = ["iniconfig"]
|
||
|
||
[build-system]
|
||
requires = ["setuptools>=42"]
|
||
build-backend = "setuptools.build_meta"
|
||
"#,
|
||
)?;
|
||
|
||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 2 packages in [TIME]
|
||
Prepared 1 package in [TIME]
|
||
Installed 1 package in [TIME]
|
||
+ my-project==0.2.0 (from file://[TEMP_DIR]/)
|
||
"###);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn sync_environment_prompt() -> Result<()> {
|
||
let context = TestContext::new_with_versions(&["3.12"]);
|
||
|
||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||
pyproject_toml.write_str(
|
||
r#"
|
||
[project]
|
||
name = "my-project"
|
||
version = "0.1.0"
|
||
requires-python = ">=3.12"
|
||
dependencies = ["iniconfig"]
|
||
"#,
|
||
)?;
|
||
|
||
// Running `uv sync` should create `.venv`
|
||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- 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
|
||
"###);
|
||
|
||
// The `pyvenv.cfg` should contain the prompt matching the project name
|
||
let pyvenv_cfg = context.read(".venv/pyvenv.cfg");
|
||
|
||
assert!(pyvenv_cfg.contains("prompt = my-project"));
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn no_binary() -> 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 = ["iniconfig"]
|
||
"#,
|
||
)?;
|
||
|
||
context.lock().assert().success();
|
||
|
||
uv_snapshot!(context.filters(), context.sync().arg("--no-binary-package").arg("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
|
||
"###);
|
||
|
||
assert!(context.temp_dir.child("uv.lock").exists());
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn no_binary_error() -> 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 = ["odrive"]
|
||
"#,
|
||
)?;
|
||
|
||
context.lock().assert().success();
|
||
|
||
uv_snapshot!(context.filters(), context.sync().arg("--no-binary-package").arg("odrive"), @r###"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 31 packages in [TIME]
|
||
error: Distribution `odrive==0.6.8 @ registry+https://pypi.org/simple` can't be installed because it is marked as `--no-binary` but has no source distribution
|
||
"###);
|
||
|
||
assert!(context.temp_dir.child("uv.lock").exists());
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn no_build() -> 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 = ["iniconfig"]
|
||
"#,
|
||
)?;
|
||
|
||
context.lock().assert().success();
|
||
|
||
uv_snapshot!(context.filters(), context.sync().arg("--no-build-package").arg("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
|
||
"###);
|
||
|
||
assert!(context.temp_dir.child("uv.lock").exists());
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn no_build_error() -> 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 = ["django_allauth==0.51.0"]
|
||
"#,
|
||
)?;
|
||
|
||
context.lock().assert().success();
|
||
|
||
uv_snapshot!(context.filters(), context.sync().arg("--no-build-package").arg("django-allauth"), @r###"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 19 packages in [TIME]
|
||
error: Distribution `django-allauth==0.51.0 @ registry+https://pypi.org/simple` can't be installed because it is marked as `--no-build` but has no binary distribution
|
||
"###);
|
||
|
||
assert!(context.temp_dir.child("uv.lock").exists());
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn sync_wheel_url_source_error() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||
pyproject_toml.write_str(
|
||
r#"
|
||
[project]
|
||
name = "uv-test"
|
||
version = "0.0.0"
|
||
requires-python = ">=3.10"
|
||
dependencies = [
|
||
"cffi @ https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl",
|
||
]
|
||
"#,
|
||
)?;
|
||
|
||
uv_snapshot!(context.filters(), context.lock(), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 3 packages in [TIME]
|
||
"###);
|
||
|
||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 3 packages in [TIME]
|
||
error: Distribution `cffi==1.17.1 @ direct+https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl` can't be installed because the binary distribution is incompatible with the current platform
|
||
|
||
hint: You're using CPython 3.12 (`cp312`), but `cffi` (v1.17.1) only has wheels with the following Python ABI tag: `cp310`
|
||
"###);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn sync_wheel_path_source_error() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
// Download a wheel.
|
||
let archive = context
|
||
.temp_dir
|
||
.child("cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl");
|
||
download_to_disk(
|
||
"https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl",
|
||
&archive,
|
||
);
|
||
|
||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||
pyproject_toml.write_str(
|
||
r#"
|
||
[project]
|
||
name = "uv-test"
|
||
version = "0.0.0"
|
||
requires-python = ">=3.10"
|
||
dependencies = ["cffi"]
|
||
|
||
[tool.uv.sources]
|
||
cffi = { path = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl" }
|
||
"#,
|
||
)?;
|
||
|
||
uv_snapshot!(context.filters(), context.lock(), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 3 packages in [TIME]
|
||
"###);
|
||
|
||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 3 packages in [TIME]
|
||
error: Distribution `cffi==1.17.1 @ path+cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl` can't be installed because the binary distribution is incompatible with the current platform
|
||
|
||
hint: You're using CPython 3.12 (`cp312`), but `cffi` (v1.17.1) only has wheels with the following Python ABI tag: `cp310`
|
||
"###);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Avoid installing dev dependencies of transitive dependencies.
|
||
#[test]
|
||
fn transitive_dev() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||
pyproject_toml.write_str(
|
||
r#"
|
||
[project]
|
||
name = "root"
|
||
version = "0.1.0"
|
||
requires-python = ">=3.12"
|
||
dependencies = ["child"]
|
||
|
||
[tool.uv]
|
||
dev-dependencies = ["anyio>3"]
|
||
|
||
[tool.uv.sources]
|
||
child = { workspace = true }
|
||
|
||
[tool.uv.workspace]
|
||
members = ["child"]
|
||
"#,
|
||
)?;
|
||
|
||
let src = context.temp_dir.child("src").child("albatross");
|
||
src.create_dir_all()?;
|
||
|
||
let init = src.child("__init__.py");
|
||
init.touch()?;
|
||
|
||
let child = context.temp_dir.child("child");
|
||
fs_err::create_dir_all(&child)?;
|
||
|
||
let pyproject_toml = child.child("pyproject.toml");
|
||
pyproject_toml.write_str(
|
||
r#"
|
||
[project]
|
||
name = "child"
|
||
version = "0.1.0"
|
||
requires-python = ">=3.12"
|
||
|
||
[build-system]
|
||
requires = ["setuptools>=42"]
|
||
build-backend = "setuptools.build_meta"
|
||
|
||
[tool.uv]
|
||
dev-dependencies = ["iniconfig>=1"]
|
||
"#,
|
||
)?;
|
||
|
||
let src = child.child("src").child("albatross");
|
||
src.create_dir_all()?;
|
||
|
||
let init = src.child("__init__.py");
|
||
init.touch()?;
|
||
|
||
uv_snapshot!(context.filters(), context.sync().arg("--dev"), @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
|
||
+ child==0.1.0 (from file://[TEMP_DIR]/child)
|
||
+ idna==3.6
|
||
+ sniffio==1.3.1
|
||
"###);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Avoid installing dev dependencies of transitive dependencies.
|
||
#[test]
|
||
fn sync_no_editable() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||
pyproject_toml.write_str(
|
||
r#"
|
||
[project]
|
||
name = "root"
|
||
version = "0.1.0"
|
||
requires-python = ">=3.12"
|
||
dependencies = ["child"]
|
||
|
||
[build-system]
|
||
requires = ["setuptools>=42"]
|
||
build-backend = "setuptools.build_meta"
|
||
|
||
[tool.uv.sources]
|
||
child = { workspace = true }
|
||
|
||
[tool.uv.workspace]
|
||
members = ["child"]
|
||
"#,
|
||
)?;
|
||
|
||
let src = context.temp_dir.child("src").child("albatross");
|
||
src.create_dir_all()?;
|
||
|
||
let init = src.child("__init__.py");
|
||
init.touch()?;
|
||
|
||
let child = context.temp_dir.child("child");
|
||
fs_err::create_dir_all(&child)?;
|
||
|
||
let pyproject_toml = child.child("pyproject.toml");
|
||
pyproject_toml.write_str(
|
||
r#"
|
||
[project]
|
||
name = "child"
|
||
version = "0.1.0"
|
||
requires-python = ">=3.12"
|
||
|
||
[build-system]
|
||
requires = ["setuptools>=42"]
|
||
build-backend = "setuptools.build_meta"
|
||
"#,
|
||
)?;
|
||
|
||
let src = child.child("src").child("child");
|
||
src.create_dir_all()?;
|
||
|
||
let init = src.child("__init__.py");
|
||
init.touch()?;
|
||
|
||
uv_snapshot!(context.filters(), context.sync().arg("--no-editable"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 2 packages in [TIME]
|
||
Prepared 2 packages in [TIME]
|
||
Installed 2 packages in [TIME]
|
||
+ child==0.1.0 (from file://[TEMP_DIR]/child)
|
||
+ root==0.1.0 (from file://[TEMP_DIR]/)
|
||
"###);
|
||
|
||
// Remove the project.
|
||
fs_err::remove_dir_all(&child)?;
|
||
|
||
// Ensure that we can still import it.
|
||
uv_snapshot!(context.filters(), context.run().arg("--no-sync").arg("python").arg("-c").arg("import child"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
"###);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
/// Check warning message for <https://github.com/astral-sh/uv/issues/6998>
|
||
/// if no `build-system` section is defined.
|
||
fn sync_scripts_without_build_system() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||
pyproject_toml.write_str(
|
||
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(
|
||
r#"
|
||
def custom_entry():
|
||
print!("Hello")
|
||
"#,
|
||
)?;
|
||
|
||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
warning: Skipping installation of entry points (`project.scripts`) because this project is not packaged; to install entry points, set `tool.uv.package = true` or define a `build-system`
|
||
Resolved 1 package in [TIME]
|
||
Audited in [TIME]
|
||
"###);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
/// Check warning message for <https://github.com/astral-sh/uv/issues/6998>
|
||
/// if the project is marked as `package = false`.
|
||
fn sync_scripts_project_not_packaged() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||
pyproject_toml.write_str(
|
||
r#"
|
||
[project]
|
||
name = "foo"
|
||
version = "0.1.0"
|
||
requires-python = ">=3.12"
|
||
dependencies = []
|
||
|
||
[project.scripts]
|
||
entry = "foo:custom_entry"
|
||
|
||
[build-system]
|
||
requires = ["hatchling"]
|
||
build-backend = "hatchling.build"
|
||
|
||
[tool.uv]
|
||
package = false
|
||
"#,
|
||
)?;
|
||
|
||
let test_script = context.temp_dir.child("src/__init__.py");
|
||
test_script.write_str(
|
||
r#"
|
||
def custom_entry():
|
||
print!("Hello")
|
||
"#,
|
||
)?;
|
||
|
||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
warning: Skipping installation of entry points (`project.scripts`) because this project is not packaged; to install entry points, set `tool.uv.package = true` or define a `build-system`
|
||
Resolved 1 package in [TIME]
|
||
Audited in [TIME]
|
||
"###);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn sync_dynamic_extra() -> 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 = ["iniconfig"]
|
||
dynamic = ["optional-dependencies"]
|
||
|
||
[tool.setuptools.dynamic.optional-dependencies]
|
||
dev = { file = "requirements-dev.txt" }
|
||
|
||
[build-system]
|
||
requires = ["setuptools>=42"]
|
||
build-backend = "setuptools.build_meta"
|
||
"#,
|
||
)?;
|
||
|
||
context
|
||
.temp_dir
|
||
.child("requirements-dev.txt")
|
||
.write_str("typing-extensions")?;
|
||
|
||
uv_snapshot!(context.filters(), context.sync().arg("--extra").arg("dev"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 3 packages in [TIME]
|
||
Prepared 3 packages in [TIME]
|
||
Installed 3 packages in [TIME]
|
||
+ iniconfig==2.0.0
|
||
+ project==0.1.0 (from file://[TEMP_DIR]/)
|
||
+ typing-extensions==4.10.0
|
||
"###);
|
||
|
||
let lock = context.read("uv.lock");
|
||
|
||
insta::with_settings!(
|
||
{
|
||
filters => context.filters(),
|
||
},
|
||
{
|
||
assert_snapshot!(
|
||
lock, @r###"
|
||
version = 1
|
||
requires-python = ">=3.12"
|
||
|
||
[options]
|
||
exclude-newer = "2024-03-25T00:00:00Z"
|
||
|
||
[[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 }
|
||
wheels = [
|
||
{ url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 },
|
||
]
|
||
|
||
[[package]]
|
||
name = "project"
|
||
version = "0.1.0"
|
||
source = { editable = "." }
|
||
dependencies = [
|
||
{ name = "iniconfig" },
|
||
]
|
||
|
||
[package.optional-dependencies]
|
||
dev = [
|
||
{ name = "typing-extensions" },
|
||
]
|
||
|
||
[package.metadata]
|
||
requires-dist = [
|
||
{ name = "iniconfig" },
|
||
{ name = "typing-extensions", marker = "extra == 'dev'" },
|
||
]
|
||
|
||
[[package]]
|
||
name = "typing-extensions"
|
||
version = "4.10.0"
|
||
source = { registry = "https://pypi.org/simple" }
|
||
sdist = { url = "https://files.pythonhosted.org/packages/16/3a/0d26ce356c7465a19c9ea8814b960f8a36c3b0d07c323176620b7b483e44/typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb", size = 77558 }
|
||
wheels = [
|
||
{ url = "https://files.pythonhosted.org/packages/f9/de/dc04a3ea60b22624b51c703a84bbe0184abcd1d0b9bc8074b5d6b7ab90bb/typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475", size = 33926 },
|
||
]
|
||
"###
|
||
);
|
||
}
|
||
);
|
||
|
||
// Check that we can re-read the lockfile.
|
||
uv_snapshot!(context.filters(), context.sync().arg("--locked"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 3 packages in [TIME]
|
||
Uninstalled 1 package in [TIME]
|
||
- typing-extensions==4.10.0
|
||
"###);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn build_system_requires_workspace() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
let build = context.temp_dir.child("backend");
|
||
build.child("pyproject.toml").write_str(
|
||
r#"
|
||
[project]
|
||
name = "backend"
|
||
version = "0.1.0"
|
||
requires-python = ">=3.12"
|
||
dependencies = ["typing-extensions>=3.10"]
|
||
|
||
[build-system]
|
||
requires = ["setuptools>=42"]
|
||
build-backend = "setuptools.build_meta"
|
||
"#,
|
||
)?;
|
||
|
||
build
|
||
.child("src")
|
||
.child("backend")
|
||
.child("__init__.py")
|
||
.write_str(indoc! { r#"
|
||
def hello() -> str:
|
||
return "Hello, world!"
|
||
"#})?;
|
||
build.child("README.md").touch()?;
|
||
|
||
let pyproject_toml = context.temp_dir.child("project").child("pyproject.toml");
|
||
pyproject_toml.write_str(
|
||
r#"
|
||
[project]
|
||
name = "project"
|
||
version = "0.1.0"
|
||
requires-python = ">=3.12"
|
||
dependencies = ["iniconfig>=1"]
|
||
|
||
[build-system]
|
||
requires = ["setuptools>=42", "backend==0.1.0"]
|
||
build-backend = "setuptools.build_meta"
|
||
|
||
[tool.uv.workspace]
|
||
members = ["../backend"]
|
||
|
||
[tool.uv.sources]
|
||
backend = { workspace = true }
|
||
"#,
|
||
)?;
|
||
|
||
context
|
||
.temp_dir
|
||
.child("project")
|
||
.child("setup.py")
|
||
.write_str(indoc! {r"
|
||
from setuptools import setup
|
||
|
||
from backend import hello
|
||
|
||
hello()
|
||
|
||
setup()
|
||
",
|
||
})?;
|
||
|
||
uv_snapshot!(context.filters(), context.sync().current_dir(context.temp_dir.child("project")), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
|
||
Creating virtual environment at: .venv
|
||
Resolved 4 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]/project)
|
||
"###);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn build_system_requires_path() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
let build = context.temp_dir.child("backend");
|
||
build.child("pyproject.toml").write_str(
|
||
r#"
|
||
[project]
|
||
name = "backend"
|
||
version = "0.1.0"
|
||
requires-python = ">=3.12"
|
||
dependencies = ["typing-extensions>=3.10"]
|
||
|
||
[build-system]
|
||
requires = ["setuptools>=42"]
|
||
build-backend = "setuptools.build_meta"
|
||
"#,
|
||
)?;
|
||
|
||
build
|
||
.child("src")
|
||
.child("backend")
|
||
.child("__init__.py")
|
||
.write_str(indoc! { r#"
|
||
def hello() -> str:
|
||
return "Hello, world!"
|
||
"#})?;
|
||
build.child("README.md").touch()?;
|
||
|
||
let pyproject_toml = context.temp_dir.child("project").child("pyproject.toml");
|
||
pyproject_toml.write_str(
|
||
r#"
|
||
[project]
|
||
name = "project"
|
||
version = "0.1.0"
|
||
requires-python = ">=3.12"
|
||
dependencies = ["iniconfig>=1"]
|
||
|
||
[build-system]
|
||
requires = ["setuptools>=42", "backend==0.1.0"]
|
||
build-backend = "setuptools.build_meta"
|
||
|
||
[tool.uv.sources]
|
||
backend = { path = "../backend" }
|
||
"#,
|
||
)?;
|
||
|
||
context
|
||
.temp_dir
|
||
.child("project")
|
||
.child("setup.py")
|
||
.write_str(indoc! {r"
|
||
from setuptools import setup
|
||
|
||
from backend import hello
|
||
|
||
hello()
|
||
|
||
setup()
|
||
",
|
||
})?;
|
||
|
||
uv_snapshot!(context.filters(), context.sync().current_dir(context.temp_dir.child("project")), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
|
||
Creating virtual environment at: .venv
|
||
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]/project)
|
||
"###);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn sync_invalid_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.12"
|
||
dependencies = ["iniconfig"]
|
||
"#,
|
||
)?;
|
||
|
||
// If the directory already exists and is not a virtual environment we should fail with an error
|
||
fs_err::create_dir(context.temp_dir.join(".venv"))?;
|
||
fs_err::write(context.temp_dir.join(".venv").join("file"), b"")?;
|
||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: Project virtual environment directory `[VENV]/` cannot be used because it is not a valid Python environment (no Python executable was found)
|
||
"###);
|
||
|
||
// But if it's just an incompatible virtual environment...
|
||
fs_err::remove_dir_all(context.temp_dir.join(".venv"))?;
|
||
uv_snapshot!(context.filters(), context.venv().arg("--python").arg("3.11"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Using CPython 3.11.[X] interpreter at: [PYTHON-3.11]
|
||
warning: The requested interpreter resolved to Python 3.11.[X], which is incompatible with the project's Python requirement: `>=3.12`
|
||
Creating virtual environment at: .venv
|
||
Activate with: source .venv/[BIN]/activate
|
||
"###);
|
||
|
||
// Even with some extraneous content...
|
||
fs_err::write(context.temp_dir.join(".venv").join("file"), b"")?;
|
||
|
||
// We can delete and use it
|
||
uv_snapshot!(context.filters(), context.sync(), @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 2 packages in [TIME]
|
||
Prepared 1 package in [TIME]
|
||
Installed 1 package in [TIME]
|
||
+ iniconfig==2.0.0
|
||
"###);
|
||
|
||
let bin = venv_bin_path(context.temp_dir.join(".venv"));
|
||
|
||
// If there's just a broken symlink, we should warn
|
||
#[cfg(unix)]
|
||
{
|
||
fs_err::remove_file(bin.join("python"))?;
|
||
fs_err::os::unix::fs::symlink(context.temp_dir.join("does-not-exist"), bin.join("python"))?;
|
||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
warning: Ignoring existing virtual environment linked to non-existent Python interpreter: .venv/[BIN]/python -> python
|
||
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
|
||
Removed virtual environment at: .venv
|
||
Creating virtual environment at: .venv
|
||
Resolved 2 packages in [TIME]
|
||
Installed 1 package in [TIME]
|
||
+ iniconfig==2.0.0
|
||
"###);
|
||
}
|
||
|
||
// But if the Python executable is missing entirely we should also fail
|
||
fs_err::remove_dir_all(&bin)?;
|
||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: Project virtual environment directory `[VENV]/` cannot be used because it is not a valid Python environment (no Python executable was found)
|
||
"###);
|
||
|
||
// But if it's not a virtual environment...
|
||
fs_err::remove_dir_all(context.temp_dir.join(".venv"))?;
|
||
uv_snapshot!(context.filters(), context.venv().arg("--python").arg("3.11"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Using CPython 3.11.[X] interpreter at: [PYTHON-3.11]
|
||
warning: The requested interpreter resolved to Python 3.11.[X], which is incompatible with the project's Python requirement: `>=3.12`
|
||
Creating virtual environment at: .venv
|
||
Activate with: source .venv/[BIN]/activate
|
||
"###);
|
||
|
||
// Which we detect by the presence of a `pyvenv.cfg` file
|
||
fs_err::remove_file(context.temp_dir.join(".venv").join("pyvenv.cfg"))?;
|
||
|
||
// Let's make sure some extraneous content isn't removed
|
||
fs_err::write(context.temp_dir.join(".venv").join("file"), b"")?;
|
||
|
||
// We should never delete it
|
||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
|
||
error: Project virtual environment directory `[VENV]/` cannot be used because it is not a compatible environment but cannot be recreated because it is not a virtual environment
|
||
"###);
|
||
|
||
context
|
||
.temp_dir
|
||
.child(".venv")
|
||
.assert(predicate::path::is_dir());
|
||
|
||
context
|
||
.temp_dir
|
||
.child(".venv")
|
||
.child("file")
|
||
.assert(predicate::path::is_file());
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Avoid validating workspace members when `--no-sources` is provided. Rather than reporting that
|
||
/// `./anyio` is missing, install `anyio` from the registry.
|
||
#[test]
|
||
fn sync_no_sources_missing_member() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||
pyproject_toml.write_str(
|
||
r#"
|
||
[project]
|
||
name = "root"
|
||
version = "0.1.0"
|
||
requires-python = ">=3.12"
|
||
dependencies = ["anyio"]
|
||
|
||
[tool.uv.sources]
|
||
anyio = { workspace = true }
|
||
|
||
[tool.uv.workspace]
|
||
members = ["anyio"]
|
||
"#,
|
||
)?;
|
||
|
||
let src = context.temp_dir.child("src").child("albatross");
|
||
src.create_dir_all()?;
|
||
|
||
let init = src.child("__init__.py");
|
||
init.touch()?;
|
||
|
||
uv_snapshot!(context.filters(), context.sync().arg("--no-sources"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 4 packages in [TIME]
|
||
Prepared 3 packages in [TIME]
|
||
Installed 3 packages in [TIME]
|
||
+ anyio==4.3.0
|
||
+ idna==3.6
|
||
+ sniffio==1.3.1
|
||
"###);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn sync_python_version() -> Result<()> {
|
||
let context: TestContext = TestContext::new_with_versions(&["3.10", "3.11", "3.12"]);
|
||
|
||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||
pyproject_toml.write_str(indoc::indoc! {r#"
|
||
[project]
|
||
name = "project"
|
||
version = "0.1.0"
|
||
requires-python = ">=3.11"
|
||
dependencies = ["anyio==3.7.0"]
|
||
"#})?;
|
||
|
||
// We should respect the project's required version, not the first on the path
|
||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Using CPython 3.11.[X] interpreter at: [PYTHON-3.11]
|
||
Creating virtual environment at: .venv
|
||
Resolved 4 packages in [TIME]
|
||
Prepared 3 packages in [TIME]
|
||
Installed 3 packages in [TIME]
|
||
+ anyio==3.7.0
|
||
+ idna==3.6
|
||
+ sniffio==1.3.1
|
||
"###);
|
||
|
||
// Unless explicitly requested...
|
||
uv_snapshot!(context.filters(), context.sync().arg("--python").arg("3.10"), @r###"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Using CPython 3.10.[X] interpreter at: [PYTHON-3.10]
|
||
error: The requested interpreter resolved to Python 3.10.[X], which is incompatible with the project's Python requirement: `>=3.11`
|
||
"###);
|
||
|
||
// But a pin should take precedence
|
||
uv_snapshot!(context.filters(), context.python_pin().arg("3.12"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
Pinned `.python-version` to `3.12`
|
||
|
||
----- stderr -----
|
||
"###);
|
||
|
||
uv_snapshot!(context.filters(), context.sync(), @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 4 packages in [TIME]
|
||
Installed 3 packages in [TIME]
|
||
+ anyio==3.7.0
|
||
+ idna==3.6
|
||
+ sniffio==1.3.1
|
||
"###);
|
||
|
||
// Create a pin that's incompatible with the project
|
||
uv_snapshot!(context.filters(), context.python_pin().arg("3.10").arg("--no-workspace"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
Updated `.python-version` from `3.12` -> `3.10`
|
||
|
||
----- stderr -----
|
||
"###);
|
||
|
||
// We should warn on subsequent uses, but respect the pinned version?
|
||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Using CPython 3.10.[X] interpreter at: [PYTHON-3.10]
|
||
error: The Python request from `.python-version` resolved to Python 3.10.[X], which is incompatible with the project's Python requirement: `>=3.11`. Use `uv python pin` to update the `.python-version` file to a compatible version.
|
||
"###);
|
||
|
||
// Unless the pin file is outside the project, in which case we should just ignore it entirely
|
||
let child_dir = context.temp_dir.child("child");
|
||
child_dir.create_dir_all().unwrap();
|
||
|
||
let pyproject_toml = child_dir.child("pyproject.toml");
|
||
pyproject_toml
|
||
.write_str(indoc::indoc! {r#"
|
||
[project]
|
||
name = "project"
|
||
version = "0.1.0"
|
||
requires-python = ">=3.11"
|
||
dependencies = ["anyio==3.7.0"]
|
||
"#})
|
||
.unwrap();
|
||
|
||
uv_snapshot!(context.filters(), context.sync().current_dir(&child_dir), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Using CPython 3.11.[X] interpreter at: [PYTHON-3.11]
|
||
Creating virtual environment at: .venv
|
||
Resolved 4 packages in [TIME]
|
||
Installed 3 packages in [TIME]
|
||
+ anyio==3.7.0
|
||
+ idna==3.6
|
||
+ sniffio==1.3.1
|
||
"###);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn sync_explicit() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||
pyproject_toml.write_str(
|
||
r#"
|
||
[project]
|
||
name = "root"
|
||
version = "0.1.0"
|
||
requires-python = ">=3.12"
|
||
dependencies = [
|
||
"idna>2",
|
||
]
|
||
|
||
[[tool.uv.index]]
|
||
name = "test"
|
||
url = "https://test.pypi.org/simple"
|
||
explicit = true
|
||
|
||
[tool.uv.sources]
|
||
idna = { index = "test" }
|
||
"#,
|
||
)?;
|
||
|
||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 2 packages in [TIME]
|
||
Prepared 1 package in [TIME]
|
||
Installed 1 package in [TIME]
|
||
+ idna==2.7
|
||
"###);
|
||
|
||
// Clear the environment.
|
||
fs_err::remove_dir_all(&context.venv)?;
|
||
|
||
// The package should be drawn from the cache.
|
||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
|
||
Creating virtual environment at: .venv
|
||
Resolved 2 packages in [TIME]
|
||
Installed 1 package in [TIME]
|
||
+ idna==2.7
|
||
"###);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Sync all members in a workspace.
|
||
#[test]
|
||
fn sync_all() -> 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", "child"]
|
||
|
||
[build-system]
|
||
requires = ["setuptools>=42"]
|
||
build-backend = "setuptools.build_meta"
|
||
|
||
[tool.uv.workspace]
|
||
members = ["child"]
|
||
|
||
[tool.uv.sources]
|
||
child = { workspace = true }
|
||
"#,
|
||
)?;
|
||
context
|
||
.temp_dir
|
||
.child("src")
|
||
.child("project")
|
||
.child("__init__.py")
|
||
.touch()?;
|
||
|
||
// Add a workspace member.
|
||
let child = context.temp_dir.child("child");
|
||
child.child("pyproject.toml").write_str(
|
||
r#"
|
||
[project]
|
||
name = "child"
|
||
version = "0.1.0"
|
||
requires-python = ">=3.12"
|
||
dependencies = ["iniconfig>=1"]
|
||
|
||
[build-system]
|
||
requires = ["setuptools>=42"]
|
||
build-backend = "setuptools.build_meta"
|
||
"#,
|
||
)?;
|
||
child
|
||
.child("src")
|
||
.child("child")
|
||
.child("__init__.py")
|
||
.touch()?;
|
||
|
||
// Generate a lockfile.
|
||
context.lock().assert().success();
|
||
|
||
// Sync all workspace members.
|
||
uv_snapshot!(context.filters(), context.sync().arg("--all-packages"), @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
|
||
+ child==0.1.0 (from file://[TEMP_DIR]/child)
|
||
+ idna==3.6
|
||
+ iniconfig==2.0.0
|
||
+ project==0.1.0 (from file://[TEMP_DIR]/)
|
||
+ sniffio==1.3.1
|
||
"###);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Sync all members in a workspace with extras attached.
|
||
#[test]
|
||
fn sync_all_extras() -> 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 = ["child"]
|
||
|
||
[project.optional-dependencies]
|
||
types = ["sniffio>1"]
|
||
async = ["anyio>3"]
|
||
|
||
[tool.uv.workspace]
|
||
members = ["child"]
|
||
|
||
[tool.uv.sources]
|
||
child = { workspace = true }
|
||
"#,
|
||
)?;
|
||
context
|
||
.temp_dir
|
||
.child("src")
|
||
.child("project")
|
||
.child("__init__.py")
|
||
.touch()?;
|
||
|
||
// Add a workspace member.
|
||
let child = context.temp_dir.child("child");
|
||
child.child("pyproject.toml").write_str(
|
||
r#"
|
||
[project]
|
||
name = "child"
|
||
version = "0.1.0"
|
||
requires-python = ">=3.12"
|
||
dependencies = ["iniconfig>=1"]
|
||
|
||
[project.optional-dependencies]
|
||
types = ["typing-extensions>=4"]
|
||
testing = ["packaging>=24"]
|
||
|
||
[build-system]
|
||
requires = ["setuptools>=42"]
|
||
build-backend = "setuptools.build_meta"
|
||
"#,
|
||
)?;
|
||
child
|
||
.child("src")
|
||
.child("child")
|
||
.child("__init__.py")
|
||
.touch()?;
|
||
|
||
// Generate a lockfile.
|
||
context.lock().assert().success();
|
||
|
||
// Sync an extra that exists in both the parent and child.
|
||
uv_snapshot!(context.filters(), context.sync().arg("--all-packages").arg("--extra").arg("types"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 8 packages in [TIME]
|
||
Prepared 4 packages in [TIME]
|
||
Installed 4 packages in [TIME]
|
||
+ child==0.1.0 (from file://[TEMP_DIR]/child)
|
||
+ iniconfig==2.0.0
|
||
+ sniffio==1.3.1
|
||
+ typing-extensions==4.10.0
|
||
"###);
|
||
|
||
// Sync an extra that only exists in the child.
|
||
uv_snapshot!(context.filters(), context.sync().arg("--all-packages").arg("--extra").arg("testing"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 8 packages in [TIME]
|
||
Prepared 1 package in [TIME]
|
||
Uninstalled 2 packages in [TIME]
|
||
Installed 1 package in [TIME]
|
||
+ packaging==24.0
|
||
- sniffio==1.3.1
|
||
- typing-extensions==4.10.0
|
||
"###);
|
||
|
||
// Sync all extras.
|
||
uv_snapshot!(context.filters(), context.sync().arg("--all-packages").arg("--all-extras"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 8 packages in [TIME]
|
||
Prepared 2 packages in [TIME]
|
||
Installed 4 packages in [TIME]
|
||
+ anyio==4.3.0
|
||
+ idna==3.6
|
||
+ sniffio==1.3.1
|
||
+ typing-extensions==4.10.0
|
||
"###);
|
||
|
||
// Sync all extras.
|
||
uv_snapshot!(context.filters(), context.sync().arg("--all-packages").arg("--all-extras").arg("--no-extra").arg("types"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 8 packages in [TIME]
|
||
Uninstalled 1 package in [TIME]
|
||
- typing-extensions==4.10.0
|
||
"###);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Sync all members in a workspace with dependency groups attached.
|
||
#[test]
|
||
fn sync_all_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 = ["child"]
|
||
|
||
[dependency-groups]
|
||
types = ["sniffio>=1"]
|
||
async = ["anyio>=3"]
|
||
|
||
[tool.uv.workspace]
|
||
members = ["child"]
|
||
|
||
[tool.uv.sources]
|
||
child = { workspace = true }
|
||
"#,
|
||
)?;
|
||
context
|
||
.temp_dir
|
||
.child("src")
|
||
.child("project")
|
||
.child("__init__.py")
|
||
.touch()?;
|
||
|
||
// Add a workspace member.
|
||
let child = context.temp_dir.child("child");
|
||
child.child("pyproject.toml").write_str(
|
||
r#"
|
||
[project]
|
||
name = "child"
|
||
version = "0.1.0"
|
||
requires-python = ">=3.12"
|
||
dependencies = ["iniconfig>=1"]
|
||
|
||
[dependency-groups]
|
||
types = ["typing-extensions>=4"]
|
||
testing = ["packaging>=24"]
|
||
|
||
[build-system]
|
||
requires = ["setuptools>=42"]
|
||
build-backend = "setuptools.build_meta"
|
||
"#,
|
||
)?;
|
||
child
|
||
.child("src")
|
||
.child("child")
|
||
.child("__init__.py")
|
||
.touch()?;
|
||
|
||
// Generate a lockfile.
|
||
context.lock().assert().success();
|
||
|
||
// Sync a group that exists in both the parent and child.
|
||
uv_snapshot!(context.filters(), context.sync().arg("--all-packages").arg("--group").arg("types"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 8 packages in [TIME]
|
||
Prepared 4 packages in [TIME]
|
||
Installed 4 packages in [TIME]
|
||
+ child==0.1.0 (from file://[TEMP_DIR]/child)
|
||
+ iniconfig==2.0.0
|
||
+ sniffio==1.3.1
|
||
+ typing-extensions==4.10.0
|
||
"###);
|
||
|
||
// Sync a group that only exists in the child.
|
||
uv_snapshot!(context.filters(), context.sync().arg("--all-packages").arg("--group").arg("testing"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 8 packages in [TIME]
|
||
Prepared 1 package in [TIME]
|
||
Uninstalled 2 packages in [TIME]
|
||
Installed 1 package in [TIME]
|
||
+ packaging==24.0
|
||
- sniffio==1.3.1
|
||
- typing-extensions==4.10.0
|
||
"###);
|
||
|
||
// Sync a group that doesn't exist.
|
||
uv_snapshot!(context.filters(), context.sync().arg("--all-packages").arg("--group").arg("foo"), @r###"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: Group `foo` is not defined in any project's `dependency-group` table
|
||
"###);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn sync_multiple_sources_index_disjoint_extras() -> 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 = []
|
||
|
||
[project.optional-dependencies]
|
||
cu118 = ["jinja2==3.1.2"]
|
||
cu124 = ["jinja2==3.1.3"]
|
||
|
||
[tool.uv]
|
||
constraint-dependencies = ["markupsafe<3"]
|
||
conflicts = [
|
||
[
|
||
{ extra = "cu118" },
|
||
{ extra = "cu124" },
|
||
],
|
||
]
|
||
|
||
[tool.uv.sources]
|
||
jinja2 = [
|
||
{ index = "torch-cu118", extra = "cu118" },
|
||
{ index = "torch-cu124", extra = "cu124" },
|
||
]
|
||
|
||
[[tool.uv.index]]
|
||
name = "torch-cu118"
|
||
url = "https://download.pytorch.org/whl/cu118"
|
||
explicit = true
|
||
|
||
[[tool.uv.index]]
|
||
name = "torch-cu124"
|
||
url = "https://download.pytorch.org/whl/cu124"
|
||
explicit = true
|
||
"#,
|
||
)?;
|
||
|
||
// Generate a lockfile.
|
||
context
|
||
.lock()
|
||
.env_remove(EnvVars::UV_EXCLUDE_NEWER)
|
||
.assert()
|
||
.success();
|
||
|
||
uv_snapshot!(context.filters(), context.sync().arg("--extra").arg("cu124").env_remove(EnvVars::UV_EXCLUDE_NEWER), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 4 packages in [TIME]
|
||
Prepared 2 packages in [TIME]
|
||
Installed 2 packages in [TIME]
|
||
+ jinja2==3.1.3
|
||
+ markupsafe==2.1.5
|
||
"###);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn sync_derivation_chain() -> 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 = ["wsgiref"]
|
||
|
||
[[tool.uv.dependency-metadata]]
|
||
name = "wsgiref"
|
||
version = "0.1.2"
|
||
dependencies = []
|
||
"#,
|
||
)?;
|
||
|
||
let filters = context
|
||
.filters()
|
||
.into_iter()
|
||
.chain([
|
||
(r"exit code: 1", "exit status: 1"),
|
||
(r"/.*/src", "/[TMP]/src"),
|
||
])
|
||
.collect::<Vec<_>>();
|
||
|
||
uv_snapshot!(filters, context.sync(), @r###"
|
||
success: false
|
||
exit_code: 1
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 2 packages in [TIME]
|
||
× Failed to build `wsgiref==0.1.2`
|
||
├─▶ The build backend returned an error
|
||
╰─▶ Call to `setuptools.build_meta:__legacy__.build_wheel` failed (exit status: 1)
|
||
|
||
[stderr]
|
||
Traceback (most recent call last):
|
||
File "<string>", line 14, in <module>
|
||
File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 325, in get_requires_for_build_wheel
|
||
return self._get_build_requires(config_settings, requirements=['wheel'])
|
||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||
File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 295, in _get_build_requires
|
||
self.run_setup()
|
||
File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 487, in run_setup
|
||
super().run_setup(setup_script=setup_script)
|
||
File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 311, in run_setup
|
||
exec(code, locals())
|
||
File "<string>", line 5, in <module>
|
||
File "[CACHE_DIR]/[TMP]/src/ez_setup/__init__.py", line 170
|
||
print "Setuptools version",version,"or greater has been installed."
|
||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||
SyntaxError: Missing parentheses in call to 'print'. Did you mean print(...)?
|
||
|
||
hint: This usually indicates a problem with the package or the build environment.
|
||
help: `wsgiref` (v0.1.2) was included because `project` (v0.1.0) depends on `wsgiref`
|
||
"###);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn sync_derivation_chain_extra() -> 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 = []
|
||
optional-dependencies = { wsgi = ["wsgiref"] }
|
||
|
||
[[tool.uv.dependency-metadata]]
|
||
name = "wsgiref"
|
||
version = "0.1.2"
|
||
dependencies = []
|
||
"#,
|
||
)?;
|
||
|
||
let filters = context
|
||
.filters()
|
||
.into_iter()
|
||
.chain([
|
||
(r"exit code: 1", "exit status: 1"),
|
||
(r"/.*/src", "/[TMP]/src"),
|
||
])
|
||
.collect::<Vec<_>>();
|
||
|
||
uv_snapshot!(filters, context.sync().arg("--extra").arg("wsgi"), @r###"
|
||
success: false
|
||
exit_code: 1
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 2 packages in [TIME]
|
||
× Failed to build `wsgiref==0.1.2`
|
||
├─▶ The build backend returned an error
|
||
╰─▶ Call to `setuptools.build_meta:__legacy__.build_wheel` failed (exit status: 1)
|
||
|
||
[stderr]
|
||
Traceback (most recent call last):
|
||
File "<string>", line 14, in <module>
|
||
File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 325, in get_requires_for_build_wheel
|
||
return self._get_build_requires(config_settings, requirements=['wheel'])
|
||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||
File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 295, in _get_build_requires
|
||
self.run_setup()
|
||
File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 487, in run_setup
|
||
super().run_setup(setup_script=setup_script)
|
||
File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 311, in run_setup
|
||
exec(code, locals())
|
||
File "<string>", line 5, in <module>
|
||
File "[CACHE_DIR]/[TMP]/src/ez_setup/__init__.py", line 170
|
||
print "Setuptools version",version,"or greater has been installed."
|
||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||
SyntaxError: Missing parentheses in call to 'print'. Did you mean print(...)?
|
||
|
||
hint: This usually indicates a problem with the package or the build environment.
|
||
help: `wsgiref` (v0.1.2) was included because `project[wsgi]` (v0.1.0) depends on `wsgiref`
|
||
"###);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn sync_derivation_chain_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 = []
|
||
|
||
[dependency-groups]
|
||
wsgi = ["wsgiref"]
|
||
|
||
[[tool.uv.dependency-metadata]]
|
||
name = "wsgiref"
|
||
version = "0.1.2"
|
||
dependencies = []
|
||
"#,
|
||
)?;
|
||
|
||
let filters = context
|
||
.filters()
|
||
.into_iter()
|
||
.chain([
|
||
(r"exit code: 1", "exit status: 1"),
|
||
(r"/.*/src", "/[TMP]/src"),
|
||
])
|
||
.collect::<Vec<_>>();
|
||
|
||
uv_snapshot!(filters, context.sync().arg("--group").arg("wsgi"), @r###"
|
||
success: false
|
||
exit_code: 1
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 2 packages in [TIME]
|
||
× Failed to build `wsgiref==0.1.2`
|
||
├─▶ The build backend returned an error
|
||
╰─▶ Call to `setuptools.build_meta:__legacy__.build_wheel` failed (exit status: 1)
|
||
|
||
[stderr]
|
||
Traceback (most recent call last):
|
||
File "<string>", line 14, in <module>
|
||
File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 325, in get_requires_for_build_wheel
|
||
return self._get_build_requires(config_settings, requirements=['wheel'])
|
||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||
File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 295, in _get_build_requires
|
||
self.run_setup()
|
||
File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 487, in run_setup
|
||
super().run_setup(setup_script=setup_script)
|
||
File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 311, in run_setup
|
||
exec(code, locals())
|
||
File "<string>", line 5, in <module>
|
||
File "[CACHE_DIR]/[TMP]/src/ez_setup/__init__.py", line 170
|
||
print "Setuptools version",version,"or greater has been installed."
|
||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||
SyntaxError: Missing parentheses in call to 'print'. Did you mean print(...)?
|
||
|
||
hint: This usually indicates a problem with the package or the build environment.
|
||
help: `wsgiref` (v0.1.2) was included because `project:wsgi` (v0.1.0) depends on `wsgiref`
|
||
"###);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// See: <https://github.com/astral-sh/uv/issues/9743>
|
||
#[test]
|
||
fn sync_stale_egg_info() -> Result<()> {
|
||
let context = TestContext::new("3.13");
|
||
|
||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||
pyproject_toml.write_str(
|
||
r#"
|
||
[project]
|
||
name = "foo"
|
||
version = "0.1.0"
|
||
requires-python = ">=3.13"
|
||
dependencies = [
|
||
"member @ git+https://github.com/astral-sh/uv-stale-egg-info-test.git#subdirectory=member",
|
||
"root @ git+https://github.com/astral-sh/uv-stale-egg-info-test.git",
|
||
]
|
||
"#,
|
||
)?;
|
||
|
||
uv_snapshot!(context.filters(), context.lock(), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 4 packages in [TIME]
|
||
"###);
|
||
|
||
let lock = context.read("uv.lock");
|
||
|
||
insta::with_settings!(
|
||
{
|
||
filters => context.filters(),
|
||
},
|
||
{
|
||
assert_snapshot!(
|
||
lock, @r###"
|
||
version = 1
|
||
requires-python = ">=3.13"
|
||
|
||
[options]
|
||
exclude-newer = "2024-03-25T00:00:00Z"
|
||
|
||
[[package]]
|
||
name = "foo"
|
||
version = "0.1.0"
|
||
source = { virtual = "." }
|
||
dependencies = [
|
||
{ name = "member" },
|
||
{ name = "root" },
|
||
]
|
||
|
||
[package.metadata]
|
||
requires-dist = [
|
||
{ name = "member", git = "https://github.com/astral-sh/uv-stale-egg-info-test.git?subdirectory=member" },
|
||
{ name = "root", git = "https://github.com/astral-sh/uv-stale-egg-info-test.git" },
|
||
]
|
||
|
||
[[package]]
|
||
name = "member"
|
||
version = "0.1.dev5+gfea1041"
|
||
source = { git = "https://github.com/astral-sh/uv-stale-egg-info-test.git?subdirectory=member#fea10416b9c479ac88fb217e14e40249b63bfbee" }
|
||
dependencies = [
|
||
{ name = "setuptools" },
|
||
]
|
||
|
||
[[package]]
|
||
name = "root"
|
||
version = "0.1.dev5+gfea1041"
|
||
source = { git = "https://github.com/astral-sh/uv-stale-egg-info-test.git#fea10416b9c479ac88fb217e14e40249b63bfbee" }
|
||
dependencies = [
|
||
{ name = "member" },
|
||
]
|
||
|
||
[[package]]
|
||
name = "setuptools"
|
||
version = "69.2.0"
|
||
source = { registry = "https://pypi.org/simple" }
|
||
sdist = { url = "https://files.pythonhosted.org/packages/4d/5b/dc575711b6b8f2f866131a40d053e30e962e633b332acf7cd2c24843d83d/setuptools-69.2.0.tar.gz", hash = "sha256:0ff4183f8f42cd8fa3acea16c45205521a4ef28f73c6391d8a25e92893134f2e", size = 2222950 }
|
||
wheels = [
|
||
{ url = "https://files.pythonhosted.org/packages/92/e1/1c8bb3420105e70bdf357d57dd5567202b4ef8d27f810e98bb962d950834/setuptools-69.2.0-py3-none-any.whl", hash = "sha256:c21c49fb1042386df081cb5d86759792ab89efca84cf114889191cd09aacc80c", size = 821485 },
|
||
]
|
||
"###
|
||
);
|
||
}
|
||
);
|
||
|
||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 4 packages in [TIME]
|
||
Prepared 3 packages in [TIME]
|
||
Installed 3 packages in [TIME]
|
||
+ member==0.1.dev5+gfea1041 (from git+https://github.com/astral-sh/uv-stale-egg-info-test.git@fea10416b9c479ac88fb217e14e40249b63bfbee#subdirectory=member)
|
||
+ root==0.1.dev5+gfea1041 (from git+https://github.com/astral-sh/uv-stale-egg-info-test.git@fea10416b9c479ac88fb217e14e40249b63bfbee)
|
||
+ setuptools==69.2.0
|
||
"###);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// See: <https://github.com/astral-sh/uv/issues/8887>
|
||
#[test]
|
||
fn sync_git_repeated_member_static_metadata() -> Result<()> {
|
||
let context = TestContext::new("3.13");
|
||
|
||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||
pyproject_toml.write_str(
|
||
r#"
|
||
[project]
|
||
name = "foo"
|
||
version = "0.1.0"
|
||
requires-python = ">=3.13"
|
||
dependencies = ["uv-git-workspace-in-root", "workspace-member-in-subdir"]
|
||
|
||
[tool.uv.sources]
|
||
uv-git-workspace-in-root = { git = "https://github.com/astral-sh/workspace-in-root-test.git" }
|
||
workspace-member-in-subdir = { git = "https://github.com/astral-sh/workspace-in-root-test.git", subdirectory = "workspace-member-in-subdir" }
|
||
"#,
|
||
)?;
|
||
|
||
uv_snapshot!(context.filters(), context.lock(), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 3 packages in [TIME]
|
||
"###);
|
||
|
||
let lock = context.read("uv.lock");
|
||
|
||
insta::with_settings!(
|
||
{
|
||
filters => context.filters(),
|
||
},
|
||
{
|
||
assert_snapshot!(
|
||
lock, @r###"
|
||
version = 1
|
||
requires-python = ">=3.13"
|
||
|
||
[options]
|
||
exclude-newer = "2024-03-25T00:00:00Z"
|
||
|
||
[[package]]
|
||
name = "foo"
|
||
version = "0.1.0"
|
||
source = { virtual = "." }
|
||
dependencies = [
|
||
{ name = "uv-git-workspace-in-root" },
|
||
{ name = "workspace-member-in-subdir" },
|
||
]
|
||
|
||
[package.metadata]
|
||
requires-dist = [
|
||
{ name = "uv-git-workspace-in-root", git = "https://github.com/astral-sh/workspace-in-root-test.git" },
|
||
{ name = "workspace-member-in-subdir", git = "https://github.com/astral-sh/workspace-in-root-test.git?subdirectory=workspace-member-in-subdir" },
|
||
]
|
||
|
||
[[package]]
|
||
name = "uv-git-workspace-in-root"
|
||
version = "0.1.0"
|
||
source = { git = "https://github.com/astral-sh/workspace-in-root-test.git#d3ab48d2338296d47e28dbb2fb327c5e2ac4ac68" }
|
||
|
||
[[package]]
|
||
name = "workspace-member-in-subdir"
|
||
version = "0.1.0"
|
||
source = { git = "https://github.com/astral-sh/workspace-in-root-test.git?subdirectory=workspace-member-in-subdir#d3ab48d2338296d47e28dbb2fb327c5e2ac4ac68" }
|
||
dependencies = [
|
||
{ name = "uv-git-workspace-in-root" },
|
||
]
|
||
"###
|
||
);
|
||
}
|
||
);
|
||
|
||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 3 packages in [TIME]
|
||
Prepared 2 packages in [TIME]
|
||
Installed 2 packages in [TIME]
|
||
+ uv-git-workspace-in-root==0.1.0 (from git+https://github.com/astral-sh/workspace-in-root-test.git@d3ab48d2338296d47e28dbb2fb327c5e2ac4ac68)
|
||
+ workspace-member-in-subdir==0.1.0 (from git+https://github.com/astral-sh/workspace-in-root-test.git@d3ab48d2338296d47e28dbb2fb327c5e2ac4ac68#subdirectory=workspace-member-in-subdir)
|
||
"###);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// See: <https://github.com/astral-sh/uv/issues/8887>
|
||
#[test]
|
||
fn sync_git_repeated_member_dynamic_metadata() -> Result<()> {
|
||
let context = TestContext::new("3.13");
|
||
|
||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||
pyproject_toml.write_str(
|
||
r#"
|
||
[project]
|
||
name = "foo"
|
||
version = "0.1.0"
|
||
requires-python = ">=3.13"
|
||
dependencies = ["package", "dependency"]
|
||
|
||
[tool.uv.sources]
|
||
package = { git = "https://git@github.com/astral-sh/uv-dynamic-metadata-test.git" }
|
||
dependency = { git = "https://git@github.com/astral-sh/uv-dynamic-metadata-test.git", subdirectory = "dependency" }
|
||
"#,
|
||
)?;
|
||
|
||
uv_snapshot!(context.filters(), context.lock(), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
warning: Missing version constraint (e.g., a lower bound) for `typing-extensions`
|
||
Resolved 5 packages in [TIME]
|
||
"###);
|
||
|
||
let lock = context.read("uv.lock");
|
||
|
||
insta::with_settings!(
|
||
{
|
||
filters => context.filters(),
|
||
},
|
||
{
|
||
assert_snapshot!(
|
||
lock, @r###"
|
||
version = 1
|
||
requires-python = ">=3.13"
|
||
|
||
[options]
|
||
exclude-newer = "2024-03-25T00:00:00Z"
|
||
|
||
[[package]]
|
||
name = "dependency"
|
||
version = "0.1.0"
|
||
source = { git = "https://github.com/astral-sh/uv-dynamic-metadata-test.git?subdirectory=dependency#6c5aa0a65db737c9e7e2e60dc865bd8087012e64" }
|
||
dependencies = [
|
||
{ name = "iniconfig" },
|
||
]
|
||
|
||
[[package]]
|
||
name = "foo"
|
||
version = "0.1.0"
|
||
source = { virtual = "." }
|
||
dependencies = [
|
||
{ name = "dependency" },
|
||
{ name = "package" },
|
||
]
|
||
|
||
[package.metadata]
|
||
requires-dist = [
|
||
{ name = "dependency", git = "https://github.com/astral-sh/uv-dynamic-metadata-test.git?subdirectory=dependency" },
|
||
{ name = "package", git = "https://github.com/astral-sh/uv-dynamic-metadata-test.git" },
|
||
]
|
||
|
||
[[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 }
|
||
wheels = [
|
||
{ url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 },
|
||
]
|
||
|
||
[[package]]
|
||
name = "package"
|
||
version = "0.1.0"
|
||
source = { git = "https://github.com/astral-sh/uv-dynamic-metadata-test.git#6c5aa0a65db737c9e7e2e60dc865bd8087012e64" }
|
||
dependencies = [
|
||
{ name = "dependency" },
|
||
{ name = "typing-extensions" },
|
||
]
|
||
|
||
[[package]]
|
||
name = "typing-extensions"
|
||
version = "4.10.0"
|
||
source = { registry = "https://pypi.org/simple" }
|
||
sdist = { url = "https://files.pythonhosted.org/packages/16/3a/0d26ce356c7465a19c9ea8814b960f8a36c3b0d07c323176620b7b483e44/typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb", size = 77558 }
|
||
wheels = [
|
||
{ url = "https://files.pythonhosted.org/packages/f9/de/dc04a3ea60b22624b51c703a84bbe0184abcd1d0b9bc8074b5d6b7ab90bb/typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475", size = 33926 },
|
||
]
|
||
"###
|
||
);
|
||
}
|
||
);
|
||
|
||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 5 packages in [TIME]
|
||
Prepared 4 packages in [TIME]
|
||
Installed 4 packages in [TIME]
|
||
+ dependency==0.1.0 (from git+https://github.com/astral-sh/uv-dynamic-metadata-test.git@6c5aa0a65db737c9e7e2e60dc865bd8087012e64#subdirectory=dependency)
|
||
+ iniconfig==2.0.0
|
||
+ package==0.1.0 (from git+https://github.com/astral-sh/uv-dynamic-metadata-test.git@6c5aa0a65db737c9e7e2e60dc865bd8087012e64)
|
||
+ typing-extensions==4.10.0
|
||
"###);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// See: <https://github.com/astral-sh/uv/issues/8887>
|
||
#[test]
|
||
fn sync_git_repeated_member_backwards_path() -> Result<()> {
|
||
let context = TestContext::new("3.13");
|
||
|
||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||
pyproject_toml.write_str(
|
||
r#"
|
||
[project]
|
||
name = "foo"
|
||
version = "0.1.0"
|
||
requires-python = ">=3.13"
|
||
dependencies = ["package", "dependency"]
|
||
|
||
[tool.uv.sources]
|
||
package = { git = "https://github.com/astral-sh/uv-backwards-path-test", subdirectory = "root" }
|
||
dependency = { git = "https://github.com/astral-sh/uv-backwards-path-test", subdirectory = "dependency" }
|
||
"#,
|
||
)?;
|
||
|
||
uv_snapshot!(context.filters(), context.lock(), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 3 packages in [TIME]
|
||
"###);
|
||
|
||
let lock = context.read("uv.lock");
|
||
|
||
insta::with_settings!(
|
||
{
|
||
filters => context.filters(),
|
||
},
|
||
{
|
||
assert_snapshot!(
|
||
lock, @r###"
|
||
version = 1
|
||
requires-python = ">=3.13"
|
||
|
||
[options]
|
||
exclude-newer = "2024-03-25T00:00:00Z"
|
||
|
||
[[package]]
|
||
name = "dependency"
|
||
version = "0.1.0"
|
||
source = { git = "https://github.com/astral-sh/uv-backwards-path-test?subdirectory=dependency#4bcc7fcd2e548c2ab7ba6b97b1c4e3ababccc7a9" }
|
||
|
||
[[package]]
|
||
name = "foo"
|
||
version = "0.1.0"
|
||
source = { virtual = "." }
|
||
dependencies = [
|
||
{ name = "dependency" },
|
||
{ name = "package" },
|
||
]
|
||
|
||
[package.metadata]
|
||
requires-dist = [
|
||
{ name = "dependency", git = "https://github.com/astral-sh/uv-backwards-path-test?subdirectory=dependency" },
|
||
{ name = "package", git = "https://github.com/astral-sh/uv-backwards-path-test?subdirectory=root" },
|
||
]
|
||
|
||
[[package]]
|
||
name = "package"
|
||
version = "0.1.0"
|
||
source = { git = "https://github.com/astral-sh/uv-backwards-path-test?subdirectory=root#4bcc7fcd2e548c2ab7ba6b97b1c4e3ababccc7a9" }
|
||
dependencies = [
|
||
{ name = "dependency" },
|
||
]
|
||
"###
|
||
);
|
||
}
|
||
);
|
||
|
||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 3 packages in [TIME]
|
||
Prepared 2 packages in [TIME]
|
||
Installed 2 packages in [TIME]
|
||
+ dependency==0.1.0 (from git+https://github.com/astral-sh/uv-backwards-path-test@4bcc7fcd2e548c2ab7ba6b97b1c4e3ababccc7a9#subdirectory=dependency)
|
||
+ package==0.1.0 (from git+https://github.com/astral-sh/uv-backwards-path-test@4bcc7fcd2e548c2ab7ba6b97b1c4e3ababccc7a9#subdirectory=root)
|
||
"###);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// The project itself is marked as an editable dependency, but under the wrong name. The project
|
||
/// is a package.
|
||
#[test]
|
||
fn mismatched_name_self_editable() -> 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 = ["foo"]
|
||
|
||
[tool.uv.sources]
|
||
foo = { path = ".", editable = true }
|
||
"#,
|
||
)?;
|
||
|
||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||
success: false
|
||
exit_code: 1
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 2 packages in [TIME]
|
||
× Failed to build `foo @ file://[TEMP_DIR]/`
|
||
╰─▶ Package metadata name `project` does not match given name `foo`
|
||
help: `foo` was included because `project` (v0.1.0) depends on `foo`
|
||
"###);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// A wheel is available in the cache, but was requested under the wrong name.
|
||
#[test]
|
||
fn mismatched_name_cached_wheel() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
// Cache the `iniconfig` wheel.
|
||
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 = ["iniconfig @ https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz"]
|
||
"#,
|
||
)?;
|
||
|
||
uv_snapshot!(context.filters(), context.sync(), @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 (from https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz)
|
||
"###);
|
||
|
||
pyproject_toml.write_str(
|
||
r#"
|
||
[project]
|
||
name = "project"
|
||
version = "0.1.0"
|
||
requires-python = ">=3.12"
|
||
dependencies = ["foo @ https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz"]
|
||
"#,
|
||
)?;
|
||
|
||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||
success: false
|
||
exit_code: 1
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
× Failed to download and build `foo @ https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz`
|
||
╰─▶ Package metadata name `iniconfig` does not match given name `foo`
|
||
"###);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Sync a Git repository that depends on a package within the same repository via a `path` source.
|
||
///
|
||
/// See: <https://github.com/astral-sh/uv/issues/9516>
|
||
#[test]
|
||
fn sync_git_path_dependency() -> Result<()> {
|
||
let context = TestContext::new("3.13");
|
||
|
||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||
pyproject_toml.write_str(
|
||
r#"
|
||
[project]
|
||
name = "foo"
|
||
version = "0.1.0"
|
||
requires-python = ">=3.13"
|
||
dependencies = ["package2"]
|
||
|
||
[tool.uv.sources]
|
||
package2 = { git = "https://git@github.com/astral-sh/uv-path-dependency-test.git", subdirectory = "package2" }
|
||
"#,
|
||
)?;
|
||
|
||
uv_snapshot!(context.filters(), context.lock(), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 3 packages in [TIME]
|
||
"###);
|
||
|
||
let lock = context.read("uv.lock");
|
||
|
||
insta::with_settings!(
|
||
{
|
||
filters => context.filters(),
|
||
},
|
||
{
|
||
assert_snapshot!(
|
||
lock, @r###"
|
||
version = 1
|
||
requires-python = ">=3.13"
|
||
|
||
[options]
|
||
exclude-newer = "2024-03-25T00:00:00Z"
|
||
|
||
[[package]]
|
||
name = "foo"
|
||
version = "0.1.0"
|
||
source = { virtual = "." }
|
||
dependencies = [
|
||
{ name = "package2" },
|
||
]
|
||
|
||
[package.metadata]
|
||
requires-dist = [{ name = "package2", git = "https://github.com/astral-sh/uv-path-dependency-test.git?subdirectory=package2" }]
|
||
|
||
[[package]]
|
||
name = "package1"
|
||
version = "0.1.0"
|
||
source = { git = "https://github.com/astral-sh/uv-path-dependency-test.git?subdirectory=package1#28781b32cf1f260cdb2c8040628079eb265202bd" }
|
||
|
||
[[package]]
|
||
name = "package2"
|
||
version = "0.1.0"
|
||
source = { git = "https://github.com/astral-sh/uv-path-dependency-test.git?subdirectory=package2#28781b32cf1f260cdb2c8040628079eb265202bd" }
|
||
dependencies = [
|
||
{ name = "package1" },
|
||
]
|
||
"###
|
||
);
|
||
}
|
||
);
|
||
|
||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 3 packages in [TIME]
|
||
Prepared 2 packages in [TIME]
|
||
Installed 2 packages in [TIME]
|
||
+ package1==0.1.0 (from git+https://github.com/astral-sh/uv-path-dependency-test.git@28781b32cf1f260cdb2c8040628079eb265202bd#subdirectory=package1)
|
||
+ package2==0.1.0 (from git+https://github.com/astral-sh/uv-path-dependency-test.git@28781b32cf1f260cdb2c8040628079eb265202bd#subdirectory=package2)
|
||
"###);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Sync a package with multiple wheels at the same version, differing only in the build tag. We
|
||
/// should choose the wheel with the highest build tag.
|
||
#[test]
|
||
fn sync_build_tag() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
// Populate the `--find-links` entries.
|
||
fs_err::create_dir_all(context.temp_dir.join("links"))?;
|
||
|
||
for entry in fs_err::read_dir(context.workspace_root.join("scripts/links"))? {
|
||
let entry = entry?;
|
||
let path = entry.path();
|
||
if path
|
||
.file_name()
|
||
.and_then(|file_name| file_name.to_str())
|
||
.is_some_and(|file_name| file_name.starts_with("build_tag-"))
|
||
{
|
||
let dest = context
|
||
.temp_dir
|
||
.join("links")
|
||
.join(path.file_name().unwrap());
|
||
fs_err::copy(&path, &dest)?;
|
||
}
|
||
}
|
||
|
||
context
|
||
.temp_dir
|
||
.child("pyproject.toml")
|
||
.write_str(&formatdoc! { r#"
|
||
[project]
|
||
name = "project"
|
||
version = "0.1.0"
|
||
requires-python = ">=3.12"
|
||
dependencies = ["build-tag"]
|
||
|
||
[tool.uv]
|
||
find-links = ["{}"]
|
||
"#,
|
||
context.temp_dir.join("links/").portable_display(),
|
||
})?;
|
||
|
||
uv_snapshot!(context.filters(), context.lock(), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 2 packages in [TIME]
|
||
"###);
|
||
|
||
let lock = fs_err::read_to_string(context.temp_dir.child("uv.lock")).unwrap();
|
||
|
||
insta::with_settings!({
|
||
filters => context.filters(),
|
||
}, {
|
||
assert_snapshot!(
|
||
lock, @r###"
|
||
version = 1
|
||
requires-python = ">=3.12"
|
||
|
||
[options]
|
||
exclude-newer = "2024-03-25T00:00:00Z"
|
||
|
||
[[package]]
|
||
name = "build-tag"
|
||
version = "1.0.0"
|
||
source = { registry = "links" }
|
||
wheels = [
|
||
{ path = "build_tag-1.0.0-1-py2.py3-none-any.whl" },
|
||
{ path = "build_tag-1.0.0-3-py2.py3-none-any.whl" },
|
||
{ path = "build_tag-1.0.0-5-py2.py3-none-any.whl" },
|
||
]
|
||
|
||
[[package]]
|
||
name = "project"
|
||
version = "0.1.0"
|
||
source = { virtual = "." }
|
||
dependencies = [
|
||
{ name = "build-tag" },
|
||
]
|
||
|
||
[package.metadata]
|
||
requires-dist = [{ name = "build-tag" }]
|
||
"###
|
||
);
|
||
});
|
||
|
||
// Re-run with `--locked`.
|
||
uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 2 packages in [TIME]
|
||
"###);
|
||
|
||
// Install from the lockfile.
|
||
uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Prepared 1 package in [TIME]
|
||
Installed 1 package in [TIME]
|
||
+ build-tag==1.0.0
|
||
"###);
|
||
|
||
// Ensure that we choose the highest build tag (5).
|
||
uv_snapshot!(context.filters(), context.run().arg("--no-sync").arg("python").arg("-c").arg("import build_tag; build_tag.main()"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
5
|
||
|
||
----- stderr -----
|
||
"###);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn url_hash_mismatch() -> 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 = ["iniconfig"]
|
||
|
||
[tool.uv.sources]
|
||
iniconfig = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz" }
|
||
"#,
|
||
)?;
|
||
|
||
// Write a lockfile with an invalid hash.
|
||
context.temp_dir.child("uv.lock").write_str(indoc! {r#"
|
||
version = 1
|
||
requires-python = ">=3.12"
|
||
|
||
[options]
|
||
exclude-newer = "2024-03-25T00:00:00Z"
|
||
|
||
[[package]]
|
||
name = "iniconfig"
|
||
version = "2.0.0"
|
||
source = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz" }
|
||
sdist = { hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b4" }
|
||
|
||
[[package]]
|
||
name = "project"
|
||
version = "0.1.0"
|
||
source = { virtual = "." }
|
||
dependencies = [
|
||
{ name = "iniconfig" },
|
||
]
|
||
|
||
[package.metadata]
|
||
requires-dist = [{ name = "iniconfig", url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz" }]
|
||
"#})?;
|
||
|
||
// Running `uv sync` should fail.
|
||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||
success: false
|
||
exit_code: 1
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 2 packages in [TIME]
|
||
× Failed to download and build `iniconfig @ https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz`
|
||
╰─▶ Hash mismatch for `iniconfig @ https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz`
|
||
|
||
Expected:
|
||
sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b4
|
||
|
||
Computed:
|
||
sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3
|
||
help: `iniconfig` was included because `project` (v0.1.0) depends on `iniconfig`
|
||
"###);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn path_hash_mismatch() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
// Download the source.
|
||
let archive = context.temp_dir.child("iniconfig-2.0.0.tar.gz");
|
||
download_to_disk(
|
||
"https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz",
|
||
&archive,
|
||
);
|
||
|
||
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 = ["iniconfig"]
|
||
|
||
[tool.uv.sources]
|
||
iniconfig = { path = "iniconfig-2.0.0.tar.gz" }
|
||
"#,
|
||
)?;
|
||
|
||
// Write a lockfile with an invalid hash.
|
||
context.temp_dir.child("uv.lock").write_str(indoc! {r#"
|
||
version = 1
|
||
requires-python = ">=3.12"
|
||
|
||
[options]
|
||
exclude-newer = "2024-03-25T00:00:00Z"
|
||
|
||
[[package]]
|
||
name = "iniconfig"
|
||
version = "2.0.0"
|
||
source = { path = "iniconfig-2.0.0.tar.gz" }
|
||
sdist = { hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b4" }
|
||
|
||
[[package]]
|
||
name = "project"
|
||
version = "0.1.0"
|
||
source = { virtual = "." }
|
||
dependencies = [
|
||
{ name = "iniconfig" },
|
||
]
|
||
|
||
[package.metadata]
|
||
requires-dist = [{ name = "iniconfig", path = "iniconfig-2.0.0.tar.gz" }]
|
||
"#})?;
|
||
|
||
// Running `uv sync` should fail.
|
||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||
success: false
|
||
exit_code: 1
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 2 packages in [TIME]
|
||
× Failed to build `iniconfig @ file://[TEMP_DIR]/iniconfig-2.0.0.tar.gz`
|
||
╰─▶ Hash mismatch for `iniconfig @ file://[TEMP_DIR]/iniconfig-2.0.0.tar.gz`
|
||
|
||
Expected:
|
||
sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b4
|
||
|
||
Computed:
|
||
sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3
|
||
help: `iniconfig` was included because `project` (v0.1.0) depends on `iniconfig`
|
||
"###);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn find_links_relative_in_config_works_from_subdir() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||
pyproject_toml.write_str(indoc! {r#"
|
||
[project]
|
||
name = "subdir_test"
|
||
version = "0.1.0"
|
||
requires-python = ">=3.12"
|
||
dependencies = ["ok==1.0.0"]
|
||
|
||
[tool.uv]
|
||
find-links = ["packages/"]
|
||
"#})?;
|
||
|
||
// Create packages/ subdirectory and copy our "offline" tqdm wheel there
|
||
let packages = context.temp_dir.child("packages");
|
||
packages.create_dir_all()?;
|
||
|
||
let wheel_src = context
|
||
.workspace_root
|
||
.join("scripts/links/ok-1.0.0-py3-none-any.whl");
|
||
let wheel_dst = packages.child("ok-1.0.0-py3-none-any.whl");
|
||
fs_err::copy(&wheel_src, &wheel_dst)?;
|
||
|
||
// Create a separate subdir, which will become our working directory
|
||
let subdir = context.temp_dir.child("subdir");
|
||
subdir.create_dir_all()?;
|
||
|
||
// Run `uv sync --offline` from subdir. We expect it to find the local wheel in ../packages/.
|
||
uv_snapshot!(context.filters(), context.sync().current_dir(&subdir).arg("--offline"), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 2 packages in [TIME]
|
||
Prepared 1 package in [TIME]
|
||
Installed 1 package in [TIME]
|
||
+ ok==1.0.0
|
||
"###);
|
||
|
||
Ok(())
|
||
}
|