Use released packse for scenario updates (#2256)

- Now that `packse` is being published to PyPI we can install it from
there.
- Tweaks the tooling around scenario updates to manage a temporary
virtual environment for you.
- Makes use of a new index URL
- Includes local version segment scenarios (supersedes
https://github.com/astral-sh/uv/pull/2022)
This commit is contained in:
Zanie Blue 2024-03-07 11:40:54 -06:00 committed by GitHub
parent b3ac0e30ec
commit fd03362520
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 1672 additions and 913 deletions

View File

@ -1,7 +1,7 @@
//! DO NOT EDIT
//!
//! Generated with scripts/scenarios/update.py
//! Scenarios from <https://github.com/zanieb/packse/tree/4f39539c1b858e28268554604e75c69e25272e5a/scenarios>
//! Generated with ./scripts/scenarios/sync.sh
//! Scenarios from <https://github.com/zanieb/packse/tree/0.3.7/scenarios>
//!
#![cfg(all(feature = "python", feature = "pypi"))]
@ -27,9 +27,9 @@ fn command(context: &TestContext, python_versions: &[&str]) -> Command {
.arg("compile")
.arg("requirements.in")
.arg("--index-url")
.arg("https://test.pypi.org/simple")
.arg("https://astral-sh.github.io/packse/0.3.7/simple-html/")
.arg("--find-links")
.arg("https://raw.githubusercontent.com/zanieb/packse/4f39539c1b858e28268554604e75c69e25272e5a/vendor/links.html")
.arg("https://raw.githubusercontent.com/zanieb/packse/0.3.7/vendor/links.html")
.arg("--cache-dir")
.arg(context.cache_dir.path())
.env("VIRTUAL_ENV", context.venv.as_os_str())
@ -46,14 +46,12 @@ fn command(context: &TestContext, python_versions: &[&str]) -> Command {
command
}
/// requires-incompatible-python-version-compatible-override
///
/// The user requires a package which requires a Python version greater than the
/// current version, but they use an alternative Python version for package
/// resolution.
///
/// ```text
/// 3f4ac9b2
/// incompatible-python-compatible-override
/// ├── environment
/// │ └── python3.9
/// ├── root
@ -64,17 +62,17 @@ fn command(context: &TestContext, python_versions: &[&str]) -> Command {
/// └── requires python>=3.10 (incompatible with environment)
/// ```
#[test]
fn requires_incompatible_python_version_compatible_override() -> Result<()> {
fn incompatible_python_compatible_override() -> Result<()> {
let context = TestContext::new("3.9");
let python_versions = &[];
// In addition to the standard filters, swap out package names for more realistic messages
let mut filters = INSTA_FILTERS.to_vec();
filters.push((r"a-3f4ac9b2", "albatross"));
filters.push((r"-3f4ac9b2", ""));
filters.push((r"incompatible-python-compatible-override-a", "albatross"));
filters.push((r"incompatible-python-compatible-override-", "pkg-"));
let requirements_in = context.temp_dir.child("requirements.in");
requirements_in.write_str("a-3f4ac9b2==1.0.0")?;
requirements_in.write_str("incompatible-python-compatible-override-a==1.0.0")?;
let output = uv_snapshot!(filters, command(&context, python_versions)
.arg("--python-version=3.11")
@ -92,21 +90,18 @@ fn requires_incompatible_python_version_compatible_override() -> Result<()> {
"###
);
output
.assert()
.success()
.stdout(predicate::str::contains("a-3f4ac9b2==1.0.0"));
output.assert().success().stdout(predicate::str::contains(
"incompatible-python-compatible-override-a==1.0.0",
));
Ok(())
}
/// requires-compatible-python-version-incompatible-override
///
/// The user requires a package which requires a compatible Python version, but they
/// request an incompatible Python version for package resolution.
///
/// ```text
/// fd6db412
/// compatible-python-incompatible-override
/// ├── environment
/// │ └── python3.11
/// ├── root
@ -117,17 +112,17 @@ fn requires_incompatible_python_version_compatible_override() -> Result<()> {
/// └── requires python>=3.10
/// ```
#[test]
fn requires_compatible_python_version_incompatible_override() -> Result<()> {
fn compatible_python_incompatible_override() -> Result<()> {
let context = TestContext::new("3.11");
let python_versions = &[];
// In addition to the standard filters, swap out package names for more realistic messages
let mut filters = INSTA_FILTERS.to_vec();
filters.push((r"a-fd6db412", "albatross"));
filters.push((r"-fd6db412", ""));
filters.push((r"compatible-python-incompatible-override-a", "albatross"));
filters.push((r"compatible-python-incompatible-override-", "pkg-"));
let requirements_in = context.temp_dir.child("requirements.in");
requirements_in.write_str("a-fd6db412==1.0.0")?;
requirements_in.write_str("compatible-python-incompatible-override-a==1.0.0")?;
let output = uv_snapshot!(filters, command(&context, python_versions)
.arg("--python-version=3.9")
@ -149,14 +144,12 @@ fn requires_compatible_python_version_incompatible_override() -> Result<()> {
Ok(())
}
/// requires-incompatible-python-version-compatible-override-no-wheels
///
/// The user requires a package which requires a incompatible Python version, but
/// they request a compatible Python version for package resolution. There are only
/// source distributions available for the package.
///
/// ```text
/// 3521037f
/// incompatible-python-compatible-override-unavailable-no-wheels
/// ├── environment
/// │ └── python3.9
/// ├── root
@ -167,17 +160,24 @@ fn requires_compatible_python_version_incompatible_override() -> Result<()> {
/// └── requires python>=3.10 (incompatible with environment)
/// ```
#[test]
fn requires_incompatible_python_version_compatible_override_no_wheels() -> Result<()> {
fn incompatible_python_compatible_override_unavailable_no_wheels() -> Result<()> {
let context = TestContext::new("3.9");
let python_versions = &[];
// In addition to the standard filters, swap out package names for more realistic messages
let mut filters = INSTA_FILTERS.to_vec();
filters.push((r"a-3521037f", "albatross"));
filters.push((r"-3521037f", ""));
filters.push((
r"incompatible-python-compatible-override-unavailable-no-wheels-a",
"albatross",
));
filters.push((
r"incompatible-python-compatible-override-unavailable-no-wheels-",
"pkg-",
));
let requirements_in = context.temp_dir.child("requirements.in");
requirements_in.write_str("a-3521037f==1.0.0")?;
requirements_in
.write_str("incompatible-python-compatible-override-unavailable-no-wheels-a==1.0.0")?;
// Since there are no wheels for the package and it is not compatible with the
// local installation, we cannot build the source distribution to determine its
@ -202,15 +202,13 @@ fn requires_incompatible_python_version_compatible_override_no_wheels() -> Resul
Ok(())
}
/// requires-incompatible-python-version-compatible-override-no-wheels-available-system
///
/// The user requires a package which requires a incompatible Python version, but
/// they request a compatible Python version for package resolution. There are only
/// source distributions available for the package. The user has a compatible Python
/// version installed elsewhere on their system.
///
/// ```text
/// c68bcf5c
/// incompatible-python-compatible-override-available-no-wheels
/// ├── environment
/// │ ├── python3.11
/// │ └── python3.9 (active)
@ -222,18 +220,24 @@ fn requires_incompatible_python_version_compatible_override_no_wheels() -> Resul
/// └── requires python>=3.10 (incompatible with environment)
/// ```
#[test]
fn requires_incompatible_python_version_compatible_override_no_wheels_available_system(
) -> Result<()> {
fn incompatible_python_compatible_override_available_no_wheels() -> Result<()> {
let context = TestContext::new("3.9");
let python_versions = &["3.11"];
// In addition to the standard filters, swap out package names for more realistic messages
let mut filters = INSTA_FILTERS.to_vec();
filters.push((r"a-c68bcf5c", "albatross"));
filters.push((r"-c68bcf5c", ""));
filters.push((
r"incompatible-python-compatible-override-available-no-wheels-a",
"albatross",
));
filters.push((
r"incompatible-python-compatible-override-available-no-wheels-",
"pkg-",
));
let requirements_in = context.temp_dir.child("requirements.in");
requirements_in.write_str("a-c68bcf5c==1.0.0")?;
requirements_in
.write_str("incompatible-python-compatible-override-available-no-wheels-a==1.0.0")?;
// Since there is a compatible Python version available on the system, it should be
// used to build the source distributions.
@ -252,22 +256,19 @@ fn requires_incompatible_python_version_compatible_override_no_wheels_available_
"###
);
output
.assert()
.success()
.stdout(predicate::str::contains("a-c68bcf5c==1.0.0"));
output.assert().success().stdout(predicate::str::contains(
"incompatible-python-compatible-override-available-no-wheels-a==1.0.0",
));
Ok(())
}
/// requires-incompatible-python-version-compatible-override-no-compatible-wheels
///
/// The user requires a package which requires a incompatible Python version, but
/// they request a compatible Python version for package resolution. There is a
/// wheel available for the package, but it does not have a compatible tag.
///
/// ```text
/// d7b25a2d
/// incompatible-python-compatible-override-no-compatible-wheels
/// ├── environment
/// │ └── python3.9
/// ├── root
@ -278,17 +279,24 @@ fn requires_incompatible_python_version_compatible_override_no_wheels_available_
/// └── requires python>=3.10 (incompatible with environment)
/// ```
#[test]
fn requires_incompatible_python_version_compatible_override_no_compatible_wheels() -> Result<()> {
fn incompatible_python_compatible_override_no_compatible_wheels() -> Result<()> {
let context = TestContext::new("3.9");
let python_versions = &[];
// In addition to the standard filters, swap out package names for more realistic messages
let mut filters = INSTA_FILTERS.to_vec();
filters.push((r"a-d7b25a2d", "albatross"));
filters.push((r"-d7b25a2d", ""));
filters.push((
r"incompatible-python-compatible-override-no-compatible-wheels-a",
"albatross",
));
filters.push((
r"incompatible-python-compatible-override-no-compatible-wheels-",
"pkg-",
));
let requirements_in = context.temp_dir.child("requirements.in");
requirements_in.write_str("a-d7b25a2d==1.0.0")?;
requirements_in
.write_str("incompatible-python-compatible-override-no-compatible-wheels-a==1.0.0")?;
// Since there are no compatible wheels for the package and it is not compatible
// with the local installation, we cannot build the source distribution to
@ -313,15 +321,13 @@ fn requires_incompatible_python_version_compatible_override_no_compatible_wheels
Ok(())
}
/// requires-incompatible-python-version-compatible-override-other-wheel
///
/// The user requires a package which requires a incompatible Python version, but
/// they request a compatible Python version for package resolution. There are only
/// source distributions available for the compatible version of the package, but
/// there is an incompatible version with a wheel available.
///
/// ```text
/// a9179f0c
/// incompatible-python-compatible-override-other-wheel
/// ├── environment
/// │ └── python3.9
/// ├── root
@ -335,17 +341,23 @@ fn requires_incompatible_python_version_compatible_override_no_compatible_wheels
/// └── requires python>=3.12 (incompatible with environment)
/// ```
#[test]
fn requires_incompatible_python_version_compatible_override_other_wheel() -> Result<()> {
fn incompatible_python_compatible_override_other_wheel() -> Result<()> {
let context = TestContext::new("3.9");
let python_versions = &[];
// In addition to the standard filters, swap out package names for more realistic messages
let mut filters = INSTA_FILTERS.to_vec();
filters.push((r"a-a9179f0c", "albatross"));
filters.push((r"-a9179f0c", ""));
filters.push((
r"incompatible-python-compatible-override-other-wheel-a",
"albatross",
));
filters.push((
r"incompatible-python-compatible-override-other-wheel-",
"pkg-",
));
let requirements_in = context.temp_dir.child("requirements.in");
requirements_in.write_str("a-a9179f0c")?;
requirements_in.write_str("incompatible-python-compatible-override-other-wheel-a")?;
// Since there are no wheels for the version of the package compatible with the
// target and it is not compatible with the local installation, we cannot build the
@ -378,13 +390,11 @@ fn requires_incompatible_python_version_compatible_override_other_wheel() -> Res
Ok(())
}
/// requires-python-patch-version-override-no-patch
///
/// The user requires a package which requires a Python version with a patch version
/// and the user provides a target version without a patch version.
///
/// ```text
/// e1884826
/// python-patch-override-no-patch
/// ├── environment
/// │ └── python3.8.18
/// ├── root
@ -395,17 +405,17 @@ fn requires_incompatible_python_version_compatible_override_other_wheel() -> Res
/// └── requires python>=3.8.4
/// ```
#[test]
fn requires_python_patch_version_override_no_patch() -> Result<()> {
fn python_patch_override_no_patch() -> Result<()> {
let context = TestContext::new("3.8.18");
let python_versions = &[];
// In addition to the standard filters, swap out package names for more realistic messages
let mut filters = INSTA_FILTERS.to_vec();
filters.push((r"a-e1884826", "albatross"));
filters.push((r"-e1884826", ""));
filters.push((r"python-patch-override-no-patch-a", "albatross"));
filters.push((r"python-patch-override-no-patch-", "pkg-"));
let requirements_in = context.temp_dir.child("requirements.in");
requirements_in.write_str("a-e1884826==1.0.0")?;
requirements_in.write_str("python-patch-override-no-patch-a==1.0.0")?;
// Since the resolver is asked to solve with 3.8, the minimum compatible Python
// requirement is treated as 3.8.0.
@ -428,13 +438,11 @@ fn requires_python_patch_version_override_no_patch() -> Result<()> {
Ok(())
}
/// requires-python-patch-version-override-patch-compatible
///
/// The user requires a package which requires a Python version with a patch version
/// and the user provides a target version with a compatible patch version.
///
/// ```text
/// 91b4bcfc
/// python-patch-override-patch-compatible
/// ├── environment
/// │ └── python3.8.18
/// ├── root
@ -445,17 +453,17 @@ fn requires_python_patch_version_override_no_patch() -> Result<()> {
/// └── requires python>=3.8.0
/// ```
#[test]
fn requires_python_patch_version_override_patch_compatible() -> Result<()> {
fn python_patch_override_patch_compatible() -> Result<()> {
let context = TestContext::new("3.8.18");
let python_versions = &[];
// In addition to the standard filters, swap out package names for more realistic messages
let mut filters = INSTA_FILTERS.to_vec();
filters.push((r"a-91b4bcfc", "albatross"));
filters.push((r"-91b4bcfc", ""));
filters.push((r"python-patch-override-patch-compatible-a", "albatross"));
filters.push((r"python-patch-override-patch-compatible-", "pkg-"));
let requirements_in = context.temp_dir.child("requirements.in");
requirements_in.write_str("a-91b4bcfc==1.0.0")?;
requirements_in.write_str("python-patch-override-patch-compatible-a==1.0.0")?;
let output = uv_snapshot!(filters, command(&context, python_versions)
.arg("--python-version=3.8.0")
@ -473,10 +481,9 @@ fn requires_python_patch_version_override_patch_compatible() -> Result<()> {
"###
);
output
.assert()
.success()
.stdout(predicate::str::contains("a-91b4bcfc==1.0.0"));
output.assert().success().stdout(predicate::str::contains(
"python-patch-override-patch-compatible-a==1.0.0",
));
Ok(())
}

File diff suppressed because it is too large Load Diff

View File

@ -1 +0,0 @@
packse-scenarios

285
scripts/scenarios/generate.py Executable file
View File

@ -0,0 +1,285 @@
#!/usr/bin/env python3
"""
Generates and updates snapshot test cases from packse scenarios.
Important:
This script is the backend called by `./scripts/scenarios/sync.sh`, consider using that
if not developing scenarios.
Requirements:
$ uv pip install -r scripts/scenarios/requirements.txt
Uses `git`, `rustfmt`, and `cargo insta test` requirements from the project.
Usage:
Regenerate the scenario test files using the given scenarios:
$ ./scripts/scenarios/generate.py <path>
Scenarios can be developed locally with the following workflow:
Serve scenarios on a local index using packse
$ packse serve <path to scenarios>
Override the uv package index and update the tests
$ UV_INDEX_URL="http://localhost:3141" ./scripts/scenarios/generate.py <path to scenarios>
If an editable version of packse is installed, this script will use its bundled scenarios by default.
Use
"""
import argparse
import importlib.metadata
import logging
import os
import subprocess
import sys
import textwrap
from pathlib import Path
TOOL_ROOT = Path(__file__).parent
TEMPLATES = TOOL_ROOT / "templates"
INSTALL_TEMPLATE = TEMPLATES / "install.mustache"
COMPILE_TEMPLATE = TEMPLATES / "compile.mustache"
PACKSE = TOOL_ROOT / "packse-scenarios"
REQUIREMENTS = TOOL_ROOT / "requirements.txt"
PROJECT_ROOT = TOOL_ROOT.parent.parent
TESTS = PROJECT_ROOT / "crates" / "uv" / "tests"
INSTALL_TESTS = TESTS / "pip_install_scenarios.rs"
COMPILE_TESTS = TESTS / "pip_compile_scenarios.rs"
CUTE_NAMES = {
"a": "albatross",
"b": "bluebird",
"c": "crow",
"d": "duck",
"e": "eagle",
"f": "flamingo",
"g": "goose",
"h": "heron",
}
try:
import packse
import packse.inspect
except ImportError:
print(
f"missing requirement `packse`: install the requirements at {REQUIREMENTS.relative_to(PROJECT_ROOT)}",
file=sys.stderr,
)
exit(1)
try:
import chevron_blue
except ImportError:
print(
f"missing requirement `chevron-blue`: install the requirements at {REQUIREMENTS.relative_to(PROJECT_ROOT)}",
file=sys.stderr,
)
exit(1)
def main(scenarios: list[Path], snapshot_update: bool = True):
# Fetch packse version
packse_version = importlib.metadata.version("packse")
debug = logging.getLogger().getEffectiveLevel() <= logging.DEBUG
if not scenarios:
if packse_version == "0.0.0":
path = packse.__development_base_path__ / "scenarios"
if path.exists():
logging.info(
"Detected development version of packse, using scenarios from %s",
path,
)
scenarios = path.glob("*.json")
else:
logging.error(
"No scenarios provided. Found development version of packse but is missing scenarios. Is it installed as an editable?"
)
sys.exit(1)
else:
logging.error("No scenarios provided, nothing to do.")
return
targets = []
for target in scenarios:
if target.is_dir():
targets.extend(target.glob("*.json"))
else:
targets.append(target)
logging.info("Loading scenario metadata...")
data = packse.inspect.inspect(
targets=targets,
no_hash=True,
)
data["scenarios"] = [
scenario
for scenario in data["scenarios"]
# Drop the example scenario
if scenario["name"] != "example"
]
# Wrap the description onto multiple lines
for scenario in data["scenarios"]:
scenario["description_lines"] = textwrap.wrap(scenario["description"], width=80)
# Wrap the expected explanation onto multiple lines
for scenario in data["scenarios"]:
expected = scenario["expected"]
expected["explanation_lines"] = (
textwrap.wrap(expected["explanation"], width=80)
if expected["explanation"]
else []
)
# TEMPORARY
# We do not yet support local version identifiers
for scenario in data["scenarios"]:
expected = scenario["expected"]
if (
scenario["name"].startswith("local-")
and scenario["name"] != "local-not-latest"
):
expected["satisfiable"] = False
expected[
"explanation"
] = "We do not have correct behavior for local version identifiers yet"
# Generate cute names for each scenario
for scenario in data["scenarios"]:
for package in scenario["packages"]:
package["cute_name"] = CUTE_NAMES[package["name"].rsplit("-")[-1]]
# Split scenarios into `install` and `compile` cases
install_scenarios = []
compile_scenarios = []
for scenario in data["scenarios"]:
if (scenario["resolver_options"] or {}).get("python") is not None:
compile_scenarios.append(scenario)
else:
install_scenarios.append(scenario)
for template, tests, scenarios in [
(INSTALL_TEMPLATE, INSTALL_TESTS, install_scenarios),
(COMPILE_TEMPLATE, COMPILE_TESTS, compile_scenarios),
]:
data = {"scenarios": scenarios}
ref = "HEAD" if packse_version == "0.0.0" else packse_version
# Add generated metadata
data[
"generated_from"
] = f"https://github.com/zanieb/packse/tree/{ref}/scenarios"
data["generated_with"] = "./scripts/scenarios/sync.sh"
data[
"vendor_links"
] = f"https://raw.githubusercontent.com/zanieb/packse/{ref}/vendor/links.html"
data["index_url"] = f"https://astral-sh.github.io/packse/{ref}/simple-html/"
# Render the template
logging.info(f"Rendering template {template.name}")
output = chevron_blue.render(
template=template.read_text(), data=data, no_escape=True, warn=True
)
# Update the test files
logging.info(
f"Updating test file at `{tests.relative_to(PROJECT_ROOT)}`...",
)
with open(tests, "wt") as test_file:
test_file.write(output)
# Format
logging.info(
"Formatting test file...",
)
subprocess.check_call(
["rustfmt", str(tests)],
stderr=subprocess.STDOUT,
stdout=sys.stderr if debug else subprocess.DEVNULL,
)
# Update snapshots
if snapshot_update:
logging.info("Updating snapshots...")
env = os.environ.copy()
env["UV_TEST_PYTHON_PATH"] = str(PROJECT_ROOT / "bin")
subprocess.call(
[
"cargo",
"insta",
"test",
"--features",
"pypi,python",
"--accept",
"--test-runner",
"nextest",
"--test",
tests.with_suffix("").name,
],
cwd=PROJECT_ROOT,
stderr=subprocess.STDOUT,
stdout=sys.stderr if debug else subprocess.DEVNULL,
env=env,
)
else:
logging.info("Skipping snapshot update")
logging.info("Done!")
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Generates and updates snapshot test cases from packse scenarios.",
)
parser.add_argument(
"scenarios",
type=Path,
nargs="*",
help="The scenario files to use",
)
parser.add_argument(
"-v",
"--verbose",
action="store_true",
help="Enable debug logging",
)
parser.add_argument(
"-q",
"--quiet",
action="store_true",
help="Disable logging",
)
parser.add_argument(
"--no-snapshot-update",
action="store_true",
help="Disable automatic snapshot updates",
)
args = parser.parse_args()
if args.quiet:
log_level = logging.CRITICAL
elif args.verbose:
log_level = logging.DEBUG
else:
log_level = logging.INFO
logging.basicConfig(level=log_level, format="%(message)s")
main(args.scenarios, snapshot_update=not args.no_snapshot_update)

View File

@ -0,0 +1,2 @@
chevron-blue
packse>=0.3.6

View File

@ -1,4 +1,69 @@
# This file was autogenerated by uv via the following command:
# uv pip compile scripts/scenarios/requirements.in -o scripts/scenarios/requirements.txt --refresh-package packse --upgrade
certifi==2024.2.2
# via requests
charset-normalizer==3.3.2
# via requests
chevron-blue==0.2.1
packse @ git+https://github.com/zanieb/packse
waitress @ git+https://github.com/zanieb/waitress@d6d764bcc970e1e50486153588eda8a92cf5b5e4
devpi-server @ git+https://github.com/zanieb/devpi@22f71acb8f08a59a098e7ad434cf388a1193fc24#subdirectory=server
# via packse
docutils==0.20.1
# via readme-renderer
editables==0.5
# via hatchling
hatchling==1.21.1
# via packse
idna==3.6
# via requests
importlib-metadata==7.0.1
# via twine
jaraco-classes==3.3.1
# via keyring
keyring==24.3.1
# via twine
markdown-it-py==3.0.0
# via rich
mdurl==0.1.2
# via markdown-it-py
more-itertools==10.2.0
# via jaraco-classes
msgspec==0.18.6
# via packse
nh3==0.2.15
# via readme-renderer
packaging==23.2
# via hatchling
packse==0.3.7
pathspec==0.12.1
# via hatchling
pkginfo==1.10.0
# via twine
pluggy==1.4.0
# via hatchling
pygments==2.17.2
# via
# readme-renderer
# rich
readme-renderer==43.0
# via twine
requests==2.31.0
# via
# requests-toolbelt
# twine
requests-toolbelt==1.0.0
# via twine
rfc3986==2.0.0
# via twine
rich==13.7.1
# via twine
setuptools==69.1.1
# via packse
trove-classifiers==2024.3.3
# via hatchling
twine==4.0.2
# via packse
urllib3==2.2.1
# via
# requests
# twine
zipp==3.17.0
# via importlib-metadata

35
scripts/scenarios/sync.sh Executable file
View File

@ -0,0 +1,35 @@
#!/usr/bin/env bash
#
# Sync test scenarios with the pinned version of packse.
#
# Usage:
#
# Install the pinned packse version in a temporary virtual environment, fetch scenarios, and regenerate test cases and snapshots:
#
# $ ./scripts/scenarios/sync.sh
#
# Additional arguments are passed to `./scripts/scenarios/generate.py`, for example:
#
# $ ./scripts/scenarios/sync.sh --verbose --no-snapshot-update
#
# For development purposes, the `./scripts/scenarios/generate.py` script can be used directly to generate
# test cases from a local set of scenarios.
set -eu
script_root="$(realpath "$(dirname "$0")")"
cd "$script_root"
echo "Setting up a temporary environment..."
uv venv
source ".venv/bin/activate"
uv pip install -r requirements.txt --refresh-package packse
echo "Fetching packse scenarios..."
packse fetch --dest "$script_root/scenarios" --force
python "$script_root/generate.py" "$script_root/scenarios" "$@"
# Cleanup
rm -r "$script_root/scenarios"

View File

@ -27,7 +27,7 @@ fn command(context: &TestContext, python_versions: &[&str]) -> Command {
.arg("compile")
.arg("requirements.in")
.arg("--index-url")
.arg("https://test.pypi.org/simple")
.arg("{{index_url}}")
.arg("--find-links")
.arg("{{vendor_links}}")
.arg("--cache-dir")
@ -48,14 +48,12 @@ fn command(context: &TestContext, python_versions: &[&str]) -> Command {
{{#scenarios}}
/// {{name}}
///
{{#description_lines}}
/// {{.}}
{{/description_lines}}
///
/// ```text
/// {{version}}
/// {{name}}
{{#tree}}
/// {{.}}
{{/tree}}
@ -70,7 +68,7 @@ fn {{module_name}}() -> Result<()> {
{{#packages}}
filters.push((r"{{name}}", "{{cute_name}}"));
{{/packages}}
filters.push((r"-{{version}}", ""));
filters.push((r"{{name}}-", "pkg-"));
let requirements_in = context.temp_dir.child("requirements.in");
{{#root.requires}}
@ -86,11 +84,11 @@ fn {{module_name}}() -> Result<()> {
{{/resolver_options.prereleases}}
{{#resolver_options.no_build}}
.arg("--only-binary")
.arg("{{.}}-{{version}}")
.arg("{{.}}")
{{/resolver_options.no_build}}
{{#resolver_options.no_binary}}
.arg("--no-binary")
.arg("{{.}}-{{version}}")
.arg("{{.}}")
{{/resolver_options.no_binary}}
{{#resolver_options.python}}
.arg("--python-version={{.}}")

View File

@ -47,7 +47,7 @@ fn command(context: &TestContext) -> Command {
.arg("pip")
.arg("install")
.arg("--index-url")
.arg("https://test.pypi.org/simple")
.arg("{{index_url}}")
.arg("--find-links")
.arg("{{vendor_links}}")
.arg("--cache-dir")
@ -67,14 +67,12 @@ fn command(context: &TestContext) -> Command {
{{#scenarios}}
/// {{name}}
///
{{#description_lines}}
/// {{.}}
{{/description_lines}}
///
/// ```text
/// {{version}}
/// {{name}}
{{#tree}}
/// {{.}}
{{/tree}}
@ -88,7 +86,7 @@ fn {{module_name}}() {
{{#packages}}
filters.push((r"{{name}}", "{{cute_name}}"));
{{/packages}}
filters.push((r"-{{version}}", ""));
filters.push((r"{{name}}-", "pkg-"));
uv_snapshot!(filters, command(&context)
{{#resolver_options.prereleases}}
@ -96,11 +94,11 @@ fn {{module_name}}() {
{{/resolver_options.prereleases}}
{{#resolver_options.no_build}}
.arg("--only-binary")
.arg("{{.}}-{{version}}")
.arg("{{.}}")
{{/resolver_options.no_build}}
{{#resolver_options.no_binary}}
.arg("--no-binary")
.arg("{{.}}-{{version}}")
.arg("{{.}}")
{{/resolver_options.no_binary}}
{{#root.requires}}
.arg("{{requirement}}")

View File

@ -1,251 +0,0 @@
#!/usr/bin/env python3
"""
Generates and updates snapshot test cases from packse scenarios.
Usage:
Regenerate the scenario test file:
$ ./scripts/scenarios/update.py
Scenarios are pinned to a specific commit. Change the `PACKSE_COMMIT` constant to update them.
Scenarios can be developed locally with the following workflow:
Install the local version of packse
$ pip install -e <path to packse>
From the packse repository, build and publish the scenarios to a local index
$ packse index up --bg
$ packse build scenarios/*
$ packse publish dist/* --index-url http://localhost:3141/packages/local --anonymous
Override the default PyPI index for uv and update the scenarios
$ UV_INDEX_URL="http://localhost:3141/packages/all/+simple" ./scripts/scenarios/update.py
Requirements:
Requires `packse` and `chevron-blue`.
$ pip install -r scripts/scenarios/requirements.txt
Also supports a local, editable requirement on `packse`.
Uses `git`, `rustfmt`, and `cargo insta test` requirements from the project.
"""
import json
import shutil
import subprocess
import sys
import textwrap
from pathlib import Path
PACKSE_COMMIT = "4f39539c1b858e28268554604e75c69e25272e5a"
TOOL_ROOT = Path(__file__).parent
TEMPLATES = TOOL_ROOT / "templates"
INSTALL_TEMPLATE = TEMPLATES / "install.mustache"
COMPILE_TEMPLATE = TEMPLATES / "compile.mustache"
PACKSE = TOOL_ROOT / "packse-scenarios"
REQUIREMENTS = TOOL_ROOT / "requirements.txt"
PROJECT_ROOT = TOOL_ROOT.parent.parent
TESTS = PROJECT_ROOT / "crates" / "uv" / "tests"
INSTALL_TESTS = TESTS / "pip_install_scenarios.rs"
COMPILE_TESTS = TESTS / "pip_compile_scenarios.rs"
CUTE_NAMES = {
"a": "albatross",
"b": "bluebird",
"c": "crow",
"d": "duck",
"e": "eagle",
"f": "flamingo",
"g": "goose",
"h": "heron",
}
try:
import packse
except ImportError:
print(
f"missing requirement `packse`: install the requirements at {REQUIREMENTS.relative_to(PROJECT_ROOT)}",
file=sys.stderr,
)
exit(1)
try:
import chevron_blue
except ImportError:
print(
f"missing requirement `chevron-blue`: install the requirements at {REQUIREMENTS.relative_to(PROJECT_ROOT)}",
file=sys.stderr,
)
exit(1)
if packse.__development_base_path__.name != "packse":
# Not a local editable installation, download latest scenarios
if PACKSE.exists():
shutil.rmtree(PACKSE)
print("Downloading scenarios from packse repository...", file=sys.stderr)
# Perform a sparse checkout where we only grab the `scenarios` folder
subprocess.check_call(
[
"git",
"clone",
"-n",
"--depth=1",
"--filter=tree:0",
"https://github.com/zanieb/packse",
str(PACKSE),
],
cwd=TOOL_ROOT,
stdout=subprocess.DEVNULL,
stderr=subprocess.STDOUT,
)
subprocess.check_call(
["git", "sparse-checkout", "set", "--no-cone", "scenarios"],
cwd=PACKSE,
stdout=subprocess.DEVNULL,
stderr=subprocess.STDOUT,
)
subprocess.check_call(
["git", "checkout", PACKSE_COMMIT],
cwd=PACKSE,
stdout=subprocess.DEVNULL,
stderr=subprocess.STDOUT,
)
scenarios_path = str(PACKSE / "scenarios")
commit = PACKSE_COMMIT
else:
print(
f"Using scenarios in packse repository at {packse.__development_base_path__}",
file=sys.stderr,
)
scenarios_path = str(packse.__development_base_path__ / "scenarios")
# Get the commit from the repository
commit = (
subprocess.check_output(
["git", "show", "-s", "--format=%H", "HEAD"],
cwd=packse.__development_base_path__,
)
.decode()
.strip()
)
if commit != PACKSE_COMMIT:
print(f"WARNING: Expected commit {PACKSE_COMMIT!r} but found {commit!r}.")
print("Loading scenario metadata...", file=sys.stderr)
data = json.loads(
subprocess.check_output(
[
sys.executable,
"-m",
"packse",
"inspect",
"--short-names",
scenarios_path,
],
)
)
data["scenarios"] = [
scenario
for scenario in data["scenarios"]
# Drop the example scenario
if scenario["name"] != "example"
]
# Wrap the description onto multiple lines
for scenario in data["scenarios"]:
scenario["description_lines"] = textwrap.wrap(scenario["description"], width=80)
# Wrap the expected explanation onto multiple lines
for scenario in data["scenarios"]:
expected = scenario["expected"]
expected["explanation_lines"] = (
textwrap.wrap(expected["explanation"], width=80)
if expected["explanation"]
else []
)
# Generate cute names for each scenario
for scenario in data["scenarios"]:
for package in scenario["packages"]:
package["cute_name"] = CUTE_NAMES[package["name"][0]]
# Split scenarios into `install` and `compile` cases
install_scenarios = []
compile_scenarios = []
for scenario in data["scenarios"]:
if (scenario["resolver_options"] or {}).get("python") is not None:
compile_scenarios.append(scenario)
else:
install_scenarios.append(scenario)
for template, tests, scenarios in [
(INSTALL_TEMPLATE, INSTALL_TESTS, install_scenarios),
(COMPILE_TEMPLATE, COMPILE_TESTS, compile_scenarios),
]:
data = {"scenarios": scenarios}
# Add generated metadata
data["generated_from"] = f"https://github.com/zanieb/packse/tree/{commit}/scenarios"
data["generated_with"] = " ".join(sys.argv)
data[
"vendor_links"
] = f"https://raw.githubusercontent.com/zanieb/packse/{commit}/vendor/links.html"
# Render the template
print(f"Rendering template {template.name}", file=sys.stderr)
output = chevron_blue.render(
template=template.read_text(), data=data, no_escape=True, warn=True
)
# Update the test files
print(
f"Updating test file at `{tests.relative_to(PROJECT_ROOT)}`...",
file=sys.stderr,
)
with open(tests, "wt") as test_file:
test_file.write(output)
# Format
print(
"Formatting test file...",
file=sys.stderr,
)
subprocess.check_call(["rustfmt", str(tests)])
# Update snapshots
print("Updating snapshots...\n", file=sys.stderr)
subprocess.call(
[
"cargo",
"insta",
"test",
"--features",
"pypi,python",
"--accept",
"--test-runner",
"nextest",
"--test",
tests.with_suffix("").name,
],
cwd=PROJECT_ROOT,
)
print("\nDone!", file=sys.stderr)