blob: 9d8827c5e46c60819c586ccd03878238a932c168 [file] [log] [blame]
#!/usr/bin/env python3
"""Upload mypy packages to PyPI.
You must first tag the release, use `git push --tags` and wait for the wheel build in CI to complete.
"""
from __future__ import annotations
import argparse
import contextlib
import json
import re
import shutil
import subprocess
import tarfile
import tempfile
import venv
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
from typing import Any, Iterator
from urllib.request import urlopen
BASE = "https://api.github.com/repos"
REPO = "mypyc/mypy_mypyc-wheels"
def is_whl_or_tar(name: str) -> bool:
return name.endswith(".tar.gz") or name.endswith(".whl")
def item_ok_for_pypi(name: str) -> bool:
if not is_whl_or_tar(name):
return False
if name.endswith(".tar.gz"):
name = name[:-7]
if name.endswith(".whl"):
name = name[:-4]
if name.endswith("wasm32"):
return False
return True
def get_release_for_tag(tag: str) -> dict[str, Any]:
with urlopen(f"{BASE}/{REPO}/releases/tags/{tag}") as f:
data = json.load(f)
assert isinstance(data, dict)
assert data["tag_name"] == tag
return data
def download_asset(asset: dict[str, Any], dst: Path) -> Path:
name = asset["name"]
assert isinstance(name, str)
download_url = asset["browser_download_url"]
assert is_whl_or_tar(name)
with urlopen(download_url) as src_file:
with open(dst / name, "wb") as dst_file:
shutil.copyfileobj(src_file, dst_file)
return dst / name
def download_all_release_assets(release: dict[str, Any], dst: Path) -> None:
print("Downloading assets...")
with ThreadPoolExecutor() as e:
for asset in e.map(lambda asset: download_asset(asset, dst), release["assets"]):
print(f"Downloaded {asset}")
def check_sdist(dist: Path, version: str) -> None:
tarfiles = list(dist.glob("*.tar.gz"))
assert len(tarfiles) == 1
sdist = tarfiles[0]
assert version in sdist.name
with tarfile.open(sdist) as f:
version_py = f.extractfile(f"{sdist.name[:-len('.tar.gz')]}/mypy/version.py")
assert version_py is not None
version_py_contents = version_py.read().decode("utf-8")
# strip a git hash from our version, if necessary, since that's not present in version.py
match = re.match(r"(.*\+dev).*$", version)
hashless_version = match.group(1) if match else version
assert (
f'"{hashless_version}"' in version_py_contents
), "Version does not match version.py in sdist"
def spot_check_dist(dist: Path, version: str) -> None:
items = [item for item in dist.iterdir() if item_ok_for_pypi(item.name)]
assert len(items) > 10
assert all(version in item.name for item in items)
assert any(item.name.endswith("py3-none-any.whl") for item in items)
@contextlib.contextmanager
def tmp_twine() -> Iterator[Path]:
with tempfile.TemporaryDirectory() as tmp_dir:
tmp_venv_dir = Path(tmp_dir) / "venv"
venv.create(tmp_venv_dir, with_pip=True)
pip_exe = tmp_venv_dir / "bin" / "pip"
subprocess.check_call([pip_exe, "install", "twine"])
yield tmp_venv_dir / "bin" / "twine"
def upload_dist(dist: Path, dry_run: bool = True) -> None:
with tmp_twine() as twine:
files = [item for item in dist.iterdir() if item_ok_for_pypi(item.name)]
cmd: list[Any] = [twine, "upload"]
cmd += files
if dry_run:
print("[dry run] " + " ".join(map(str, cmd)))
else:
print(" ".join(map(str, cmd)))
subprocess.check_call(cmd)
def upload_to_pypi(version: str, dry_run: bool = True) -> None:
assert re.match(r"v?[1-9]\.[0-9]+\.[0-9](\+\S+)?$", version)
if "dev" in version:
assert dry_run, "Must use --dry-run with dev versions of mypy"
if version.startswith("v"):
version = version[1:]
target_dir = tempfile.mkdtemp()
dist = Path(target_dir) / "dist"
dist.mkdir()
print(f"Temporary target directory: {target_dir}")
release = get_release_for_tag(f"v{version}")
download_all_release_assets(release, dist)
spot_check_dist(dist, version)
check_sdist(dist, version)
upload_dist(dist, dry_run)
print("<< All done! >>")
def main() -> None:
parser = argparse.ArgumentParser(description="PyPI mypy package uploader")
parser.add_argument(
"--dry-run", action="store_true", default=False, help="Don't actually upload packages"
)
parser.add_argument("version", help="mypy version to release")
args = parser.parse_args()
upload_to_pypi(args.version, args.dry_run)
if __name__ == "__main__":
main()