|
1 | 1 | #!/usr/bin/env python3 |
2 | 2 | import contextlib |
| 3 | +import dataclasses |
3 | 4 | import os |
4 | 5 | import subprocess |
| 6 | +import sys |
| 7 | +import tempfile |
5 | 8 | from collections.abc import Iterator |
| 9 | +from concurrent.futures import ThreadPoolExecutor |
| 10 | +from concurrent.futures import wait |
6 | 11 | from pathlib import Path |
7 | 12 |
|
8 | 13 | import click |
9 | 14 |
|
10 | 15 |
|
11 | | -def run(*args: str) -> None: |
12 | | - cmd = " ".join(args) |
13 | | - click.echo("{} {}".format(click.style(">", fg="blue", bold=True), cmd)) |
| 16 | +@dataclasses.dataclass |
| 17 | +class VirtualEnv: |
| 18 | + |
| 19 | + path: Path |
| 20 | + |
| 21 | + def run(self, *args: object) -> None: |
| 22 | + python = self.path / "bin" / "python" |
| 23 | + run(python, *args) |
| 24 | + |
14 | 25 |
|
15 | | - verbose = click.get_current_context().meta["verbose"] |
16 | | - process = subprocess.run(args, capture_output=(not verbose)) |
| 26 | +def run(*args: object) -> None: |
| 27 | + args = [str(arg) for arg in args] |
| 28 | + cmd = " ".join(args) |
| 29 | + ctx = click.get_current_context() |
| 30 | + |
| 31 | + if not ctx.meta.get("quiet", False): |
| 32 | + click.echo("{} {}".format(click.style(">", fg="blue", bold=True), cmd)) |
| 33 | + |
| 34 | + verbose = ctx.meta.get("verbose", False) |
| 35 | + process = subprocess.run( |
| 36 | + args, |
| 37 | + stdout=subprocess.PIPE if not verbose else None, |
| 38 | + stderr=subprocess.STDOUT if not verbose else None, |
| 39 | + check=False, |
| 40 | + ) |
17 | 41 | if process.returncode != 0: |
18 | 42 | click.echo( |
19 | 43 | "{} {} failed with returncode {}".format(click.style("!", fg="red", bold=True), cmd, process.returncode), |
20 | 44 | err=True, |
21 | 45 | ) |
22 | 46 |
|
23 | | - if process.stderr or process.stdout: |
24 | | - click.echo(process.stdout.decode()) |
25 | | - click.echo(process.stderr.decode(), err=True) |
| 47 | + if process.stdout: |
| 48 | + click.echo(process.stdout.decode(), err=True) |
26 | 49 | raise click.ClickException(f"Command failed with return code {process.returncode}") |
27 | 50 |
|
28 | 51 |
|
29 | | -def python(venv: Path, *args: str) -> None: |
30 | | - pybinary = venv / "bin" / "python" |
31 | | - run(str(pybinary), *args) |
| 52 | +BUILD_COMMAND_ARG = { |
| 53 | + "sdist": "-s", |
| 54 | + "wheel": "-w", |
| 55 | +} |
| 56 | + |
| 57 | +BUILD_ARTIFACT_PATTERN = { |
| 58 | + "sdist": "*.tar.gz", |
| 59 | + "wheel": "*.whl", |
| 60 | +} |
32 | 61 |
|
33 | 62 |
|
34 | | -def dist(location: Path, pattern: str) -> Path: |
| 63 | +def find_dist(location: Path, pattern: str) -> Path: |
35 | 64 | candidates = sorted(location.glob(pattern), key=lambda p: p.stat().st_mtime, reverse=True) |
36 | 65 | if not candidates: |
37 | | - raise click.ClickException("No sdist found") |
| 66 | + raise click.ClickException(f"No {pattern} found") |
38 | 67 | return candidates[0] |
39 | 68 |
|
40 | 69 |
|
41 | | -def clean(package: str) -> None: |
42 | | - run("rm", "-rf", f"src/{package}/assets/*") |
43 | | - run("rm", "-rf", "dist") |
44 | | - |
45 | | - |
46 | 70 | @contextlib.contextmanager |
47 | | -def virtualenv(root: Path, name: str) -> Iterator[Path]: |
| 71 | +def virtualenv(root: Path, name: str) -> Iterator[VirtualEnv]: |
| 72 | + """Create a virtualenv and yield the path to it.""" |
48 | 73 | run("python", "-m", "venv", str(root / name)) |
49 | | - yield root / name |
| 74 | + yield VirtualEnv(root / name) |
50 | 75 | run("rm", "-rf", str(root / name)) |
51 | 76 |
|
52 | 77 |
|
53 | | -def check(venv: Path, package: str, assets: bool = False) -> None: |
54 | | - python(venv, "-c", f"import {package}; print({package}.__version__)") |
55 | | - |
56 | | - if assets: |
57 | | - python(venv, "-c", f"import {package}.assets; {package}.assets.check_dist()") |
| 78 | +def check_dist(ctx: click.Context, package: str, dist: str, assets: bool = False) -> None: |
58 | 79 |
|
59 | | - python(venv, "-m", "pip", "install", "twine") |
60 | | - python(venv, "-m", "twine", "check", f"dist/{package}-*") |
| 80 | + with ctx.scope(), tempfile.TemporaryDirectory() as tmp_directory: |
| 81 | + tmpdir = Path(tmp_directory) |
| 82 | + distdir = tmpdir / "dist" |
| 83 | + run(sys.executable, "-m", "build", BUILD_COMMAND_ARG[dist], ".", "--outdir", distdir) |
61 | 84 |
|
| 85 | + with virtualenv(tmpdir, "venv-dist") as venv: |
| 86 | + venv.run("-m", "pip", "install", "--upgrade", "pip") |
| 87 | + sdist = find_dist(distdir, BUILD_ARTIFACT_PATTERN[dist]) |
| 88 | + venv.run("-m", "pip", "install", str(sdist)) |
62 | 89 |
|
63 | | -def sdist(package: str, assets: bool = False) -> None: |
64 | | - clean(package=package) |
65 | | - run("python", "-m", "build", "-s", ".") |
66 | | - with virtualenv(Path("dist"), "venv-sdist") as venv: |
67 | | - python(venv, "-m", "pip", "install", "--upgrade", "pip") |
68 | | - sdist = dist(Path("dist/"), "*.tar.gz") |
69 | | - python(venv, "-m", "pip", "install", str(sdist)) |
70 | | - check(venv, package, assets=assets) |
71 | | - click.secho("sdist built and installed successfully", fg="green", bold=True) |
| 90 | + venv.run("-c", f"import {package}; print({package}.__version__)") |
| 91 | + if assets: |
| 92 | + venv.run("-c", f"import {package}.assets; {package}.assets.check_dist()") |
72 | 93 |
|
| 94 | + with virtualenv(tmpdir, "venv-twine") as venv: |
| 95 | + venv.run("-m", "pip", "install", "twine") |
| 96 | + venv.run("-m", "twine", "check", sdist) |
73 | 97 |
|
74 | | -def wheel(package: str, assets: bool = False) -> None: |
75 | | - clean(package=package) |
76 | | - run("python", "-m", "build", "-w", ".") |
77 | | - with virtualenv(Path("dist"), "venv-wheel") as venv: |
78 | | - wheel = dist(Path("dist/"), "*.whl") |
79 | | - python(venv, "-m", "pip", "install", str(wheel)) |
80 | | - check(venv, package, assets=assets) |
81 | | - click.secho("wheel built and installed successfully", fg="green", bold=True) |
| 98 | + click.secho(f"{dist} built and installed successfully", fg="green", bold=True) |
82 | 99 |
|
83 | 100 |
|
84 | 101 | @click.command() |
85 | 102 | @click.option("-v", "--verbose", is_flag=True, help="Enable verbose output") |
| 103 | +@click.option("-q", "--quiet", is_flag=True, help="Enable quiet output") |
86 | 104 | @click.option("-a", "--assets", is_flag=True, help="Check assets") |
87 | | -@click.argument("package", type=str) |
| 105 | +@click.option("-t", "--timeout", default=60.0, help="Timeout for checking distribution") |
| 106 | +@click.argument("toxinidir", type=str, required=True) |
88 | 107 | @click.pass_context |
89 | | -def main(ctx: click.Context, package: str, verbose: bool, assets: bool) -> None: |
| 108 | +def main(ctx: click.Context, toxinidir: str, verbose: bool, quiet: bool, assets: bool, timeout: float) -> None: |
90 | 109 | """Check distribution for package""" |
91 | 110 | if os.environ.get("CI") == "true": |
92 | 111 | verbose = True |
93 | 112 |
|
| 113 | + ctx.meta["quiet"] = quiet |
94 | 114 | ctx.meta["verbose"] = verbose |
95 | | - sdist(package, assets=assets) |
96 | | - wheel(package, assets=assets) |
| 115 | + |
| 116 | + package = Path(toxinidir).name |
| 117 | + click.secho(f"Checking distribution for {package}", bold=True) |
| 118 | + |
| 119 | + with ThreadPoolExecutor() as executor: |
| 120 | + sdist = executor.submit(check_dist, ctx, package, "sdist", assets=assets) |
| 121 | + wheel = executor.submit(check_dist, ctx, package, "wheel", assets=assets) |
| 122 | + |
| 123 | + done, _ = wait([sdist, wheel], return_when="ALL_COMPLETED", timeout=timeout) |
| 124 | + |
| 125 | + for future in done: |
| 126 | + future.result() |
97 | 127 |
|
98 | 128 |
|
99 | 129 | if __name__ == "__main__": |
|
0 commit comments