|
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