mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-07 13:25:00 +00:00

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` ```
422 lines
13 KiB
Python
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()
|