uv/scripts/registries-test.py
John Mumm 2fc922144a
Add script for testing uv against different registries (#13615)
This PR provides a script that uses environment variables to determine
which registries to test. This script is being used to run automated
registry tests in CI for AWS, Azure, GCP, Artifactory, GitLab,
Cloudsmith, and Gemfury.

You must configure the following required env vars for each registry:
```
    UV_TEST_<registry_name>_URL            URL for the registry
    UV_TEST_<registry_name>_TOKEN       authentication token
    UV_TEST_<registry_name>_PKG          private package to install
```

The username defaults to "\_\_token\_\_" but can be optionally set with:
```
    UV_TEST_<registry_name>_USERNAME
```

For each configured registry, the test will attempt to install the
specified package. Some registries can fall back to PyPI internally, so
it's important to choose a package that only exists in the registry you
are testing.

Currently, a successful test means that it finds the line “ +
<package_name>” in the output. This is because in its current form we
don’t know ahead of time what package it is and hence what the exact
expected output would be. The advantage if that anyone can run this
locally, though they would have to have access to the registries they
want to test.

You can also use the `--use-op` command line argument to derive these
test env vars from a 1Password vault (default is "RegistryTests" but can
be configured with `--op-vault`). It will look at all items in the vault
with names following the pattern `UV_TEST_<registry_name>` and will
derive the env vars as follows:

```
    `UV_TEST_<registry_name>_USERNAME` from the `username` field
    `UV_TEST_<registry_name>_TOKEN` from the `password` field
    `UV_TEST_<registry_name>_URL` from a field with the label `url`
    `UV_TEST_<registry_name>_PKG` from a field with the label `pkg`
```
2025-06-18 09:43:45 +02:00

422 lines
13 KiB
Python

#!/usr/bin/env python3
"""
Test `uv add` against multiple Python package registries.
This script looks for environment variables that configure registries for testing.
To configure a registry, set the following environment variables:
`UV_TEST_<registry_name>_URL` URL for the registry
`UV_TEST_<registry_name>_TOKEN` authentication token
The username defaults to "__token__" but can be optionally set with:
`UV_TEST_<registry_name>_USERNAME`
The package to install defaults to "astral-registries-test-pkg" but can be optionally
set with:
`UV_TEST_<registry_name>_PKG`
Keep in mind that some registries can fall back to PyPI internally, so make sure
you choose a package that only exists in the registry you are testing.
You can also use the 1Password CLI to fetch registry credentials from a vault by passing
the `--use-op` flag. For each item in the vault named `UV_TEST_XXX`, the script will set
env vars for any of the following fields, if present:
`UV_TEST_<registry_name>_USERNAME` from the `username` field
`UV_TEST_<registry_name>_TOKEN` from the `password` field
`UV_TEST_<registry_name>_URL` from a field with the label `url`
`UV_TEST_<registry_name>_PKG` from a field with the label `pkg`
# /// script
# requires-python = ">=3.12"
# dependencies = ["colorama>=0.4.6"]
# ///
"""
import argparse
import json
import os
import re
import subprocess
import sys
import tempfile
from pathlib import Path
from typing import Dict
import colorama
from colorama import Fore
def initialize_colorama(force_color=False):
colorama.init(strip=not force_color, autoreset=True)
cwd = Path(__file__).parent
DEFAULT_TIMEOUT = 30
DEFAULT_PKG_NAME = "astral-registries-test-pkg"
KNOWN_REGISTRIES = [
"artifactory",
"azure",
"aws",
"cloudsmith",
"gcp",
"gemfury",
"gitlab",
]
def fetch_op_items(vault_name: str, env: Dict[str, str]) -> Dict[str, str]:
"""Fetch items from the specified 1Password vault and add them to the environment.
For each item named UV_TEST_XXX in the vault:
- Set `UV_TEST_XXX_USERNAME` to the `username` field
- Set `UV_TEST_XXX_TOKEN` to the `password` field
- Set `UV_TEST_XXX_URL` to the `url` field
Raises exceptions for any 1Password CLI errors so they can be handled by the caller.
"""
# Run 'op item list' to get all items in the vault
result = subprocess.run(
["op", "item", "list", "--vault", vault_name, "--format", "json"],
capture_output=True,
text=True,
check=True,
)
items = json.loads(result.stdout)
updated_env = env.copy()
for item in items:
item_id = item["id"]
item_title = item["title"]
# Only process items that match the registry naming pattern
if item_title.startswith("UV_TEST_"):
# Extract the registry name (e.g., "AWS" from "UV_TEST_AWS")
registry_name = item_title.removeprefix("UV_TEST_")
# Get the item details
item_details = subprocess.run(
["op", "item", "get", item_id, "--format", "json"],
capture_output=True,
text=True,
check=True,
)
item_data = json.loads(item_details.stdout)
username = None
password = None
url = None
pkg = None
if "fields" in item_data:
for field in item_data["fields"]:
if field.get("id") == "username":
username = field.get("value")
elif field.get("id") == "password":
password = field.get("value")
elif field.get("label") == "url":
url = field.get("value")
elif field.get("label") == "pkg":
pkg = field.get("value")
if username:
updated_env[f"UV_TEST_{registry_name}_USERNAME"] = username
if password:
updated_env[f"UV_TEST_{registry_name}_TOKEN"] = password
if url:
updated_env[f"UV_TEST_{registry_name}_URL"] = url
if pkg:
updated_env[f"UV_TEST_{registry_name}_PKG"] = pkg
print(f"Added 1Password credentials for {registry_name}")
return updated_env
def get_registries(env: Dict[str, str]) -> Dict[str, str]:
pattern = re.compile(r"^UV_TEST_(.+)_URL$")
registries: Dict[str, str] = {}
for env_var, value in env.items():
match = pattern.match(env_var)
if match:
registry_name = match.group(1).lower()
registries[registry_name] = value
return registries
def setup_test_project(
registry_name: str, registry_url: str, project_dir: str, requires_python: str
):
"""Create a temporary project directory with a pyproject.toml"""
pyproject_content = f"""[project]
name = "{registry_name}-test"
version = "0.1.0"
description = "Test registry"
requires-python = ">={requires_python}"
[[tool.uv.index]]
name = "{registry_name}"
url = "{registry_url}"
default = true
"""
pyproject_file = Path(project_dir) / "pyproject.toml"
pyproject_file.write_text(pyproject_content)
def run_test(
env: dict[str, str],
uv: Path,
registry_name: str,
registry_url: str,
package: str,
username: str,
token: str,
verbosity: int,
timeout: int,
requires_python: str,
) -> bool:
print(uv)
"""Attempt to install a package from this registry."""
print(
f"{registry_name} -- Running test for {registry_url} with username {username}"
)
if package == DEFAULT_PKG_NAME:
print(
f"** Using default test package name: {package}. To choose a different package, set UV_TEST_{registry_name.upper()}_PKG"
)
print(f"\nAttempting to install {package}")
env[f"UV_INDEX_{registry_name.upper()}_USERNAME"] = username
env[f"UV_INDEX_{registry_name.upper()}_PASSWORD"] = token
with tempfile.TemporaryDirectory() as project_dir:
setup_test_project(registry_name, registry_url, project_dir, requires_python)
cmd = [
uv,
"add",
package,
"--directory",
project_dir,
]
if verbosity:
cmd.extend(["-" + "v" * verbosity])
result = None
try:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=timeout,
check=False,
env=env,
)
if result.returncode != 0:
error_msg = result.stderr.strip() if result.stderr else "Unknown error"
print(f"{Fore.RED}{registry_name}: FAIL{Fore.RESET} \n\n{error_msg}")
return False
success = False
for line in result.stderr.strip().split("\n"):
if line.startswith(f" + {package}=="):
success = True
if success:
print(f"{Fore.GREEN}{registry_name}: PASS")
if verbosity > 0:
print(f" stdout: {result.stdout.strip()}")
print(f" stderr: {result.stderr.strip()}")
return True
else:
print(
f"{Fore.RED}{registry_name}: FAIL{Fore.RESET} - Failed to install {package}."
)
except subprocess.TimeoutExpired:
print(f"{Fore.RED}{registry_name}: TIMEOUT{Fore.RESET} (>{timeout}s)")
except FileNotFoundError:
print(f"{Fore.RED}{registry_name}: ERROR{Fore.RESET} - uv not found")
except Exception as e:
print(f"{Fore.RED}{registry_name}: ERROR{Fore.RESET} - {e}")
if result:
if result.stdout:
print(f"{Fore.RED} stdout:{Fore.RESET} {result.stdout.strip()}")
if result.stderr:
print(f"\n{Fore.RED} stderr:{Fore.RESET} {result.stderr.strip()}")
return False
def parse_args() -> argparse.Namespace:
"""Parse command line arguments"""
parser = argparse.ArgumentParser(
description="Test uv add command against multiple registries",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
"--all",
action="store_true",
help="fail if any known registry was not tested",
)
parser.add_argument(
"--uv",
type=str,
help="specify a path to the uv binary (default: uv command)",
)
parser.add_argument(
"--timeout",
type=int,
default=os.environ.get("UV_TEST_TIMEOUT", DEFAULT_TIMEOUT),
help=f"timeout in seconds for each test (default: {DEFAULT_TIMEOUT} or UV_TEST_TIMEOUT)",
)
parser.add_argument(
"-v",
"--verbose",
action="count",
default=0,
help="increase verbosity (-v for debug, -vv for trace)",
)
parser.add_argument(
"--use-op",
action="store_true",
help="use 1Password CLI to fetch registry credentials from the specified vault",
)
parser.add_argument(
"--op-vault",
type=str,
default="RegistryTests",
help="name of the 1Password vault to use (default: RegistryTests)",
)
parser.add_argument(
"--required-python",
type=str,
default="3.12",
help="minimum Python version for tests (default: 3.12)",
)
parser.add_argument("--color", choices=["always", "auto", "never"], default="auto")
return parser.parse_args()
def main() -> None:
args = parse_args()
env = os.environ.copy()
if args.color == "always":
initialize_colorama(force_color=True)
elif args.color == "never":
initialize_colorama(force_color=False)
else:
initialize_colorama(force_color=sys.stdout.isatty())
# If using 1Password, fetch credentials from the vault
if args.use_op:
print(f"Fetching credentials from 1Password vault '{args.op_vault}'...")
try:
env = fetch_op_items(args.op_vault, env)
except Exception as e:
print(f"{Fore.RED}Error accessing 1Password: {e}{Fore.RESET}")
print(
f"{Fore.YELLOW}Hint: If you're not authenticated, run 'op signin' first.{Fore.RESET}"
)
sys.exit(1)
if args.uv:
# We change the working directory for the subprocess calls, so we have to
# absolutize the path.
uv = Path.cwd().joinpath(args.uv)
else:
subprocess.run(["cargo", "build"])
executable_suffix = ".exe" if os.name == "nt" else ""
uv = cwd.parent.joinpath(f"target/debug/uv{executable_suffix}")
passed = []
failed = []
skipped = []
untested_registries = set(KNOWN_REGISTRIES)
print("Running tests...")
for registry_name, registry_url in get_registries(env).items():
print("----------------")
token = env.get(f"UV_TEST_{registry_name.upper()}_TOKEN")
if not token:
if args.all:
print(
f"{Fore.RED}{registry_name}: UV_TEST_{registry_name.upper()}_TOKEN contained no token. Required by --all"
)
failed.append(registry_name)
else:
print(
f"{Fore.YELLOW}{registry_name}: UV_TEST_{registry_name.upper()}_TOKEN contained no token. Skipping test"
)
skipped.append(registry_name)
continue
# The private package we will test installing
package = env.get(f"UV_TEST_{registry_name.upper()}_PKG", DEFAULT_PKG_NAME)
username = env.get(f"UV_TEST_{registry_name.upper()}_USERNAME", "__token__")
if run_test(
env,
uv,
registry_name,
registry_url,
package,
username,
token,
args.verbose,
args.timeout,
args.required_python,
):
passed.append(registry_name)
else:
failed.append(registry_name)
untested_registries.remove(registry_name)
total = len(passed) + len(failed)
print("----------------")
if passed:
print(f"\n{Fore.GREEN}Passed:")
for registry_name in passed:
print(f" * {registry_name}")
if failed:
print(f"\n{Fore.RED}Failed:")
for registry_name in failed:
print(f" * {registry_name}")
if skipped:
print(f"\n{Fore.YELLOW}Skipped:")
for registry_name in skipped:
print(f" * {registry_name}")
print(f"\nResults: {len(passed)}/{total} tests passed, {len(skipped)} skipped")
if args.all and len(untested_registries) > 0:
print(
f"\n{Fore.RED}Failed to test all known registries (requested via --all).{Fore.RESET}\nMissing:"
)
for registry_name in untested_registries:
print(f" * {registry_name}")
print("You must use the exact registry name as listed here")
sys.exit(1)
if total == 0:
print("\nNo tests were run - have you defined at least one registry?")
print(" * UV_TEST_<registry_name>_URL")
print(" * UV_TEST_<registry_name>_TOKEN")
print(
" * UV_TEST_<registry_name>_PKG (the private package to test installing)"
)
print(' * UV_TEST_<registry_name>_USERNAME (defaults to "__token__")')
sys.exit(1)
sys.exit(0 if len(failed) == 0 else 1)
if __name__ == "__main__":
main()