diff --git a/.flake8 b/.flake8 index 1fe233e..6d517f3 100644 --- a/.flake8 +++ b/.flake8 @@ -1,15 +1,15 @@ [flake8] select = B, E, F, W, B9, ISC ignore = + B902 E203 E402 E501 E704 + E711 + E712 E722 W503 W504 - E712,E711 - B902 max-line-length = 120 -min_python_version = 3.11 -exclude = src/dominate-stubs +min_python_version = 3.11.0 diff --git a/justfile b/justfile index a640d40..c460f92 100644 --- a/justfile +++ b/justfile @@ -1,27 +1,44 @@ +# Common Flask project tasks # Set up the virtual environment based on the direnv convention # https://direnv.net/docs/legacy.html#virtualenv -virtual_env := justfile_directory() / ".direnv/python-3.11/bin" +python_version := env('PYTHON_VERSION', "3.12") +virtual_env := justfile_directory() / ".direnv/python-$python_version/bin" export PATH := virtual_env + ":" + env('PATH') +export REQUIREMENTS_TXT := env('REQUIREMENTS', '') [private] prepare: pip install --quiet --upgrade pip - pip install --quiet pip-tools pip-compile-multi + pip install --quiet -r requirements/pip-tools.txt +# lock the requirements files +compile: prepare + pip-compile-multi --use-cache --backtracking + +# Install dependencies sync: prepare - pip-compile-multi --use-cache pip-sync requirements/dev.txt - pip install -e . - tox --notest + [[ -f requirements/local.txt ]] && pip install -r requirements/local.txt + tox -p auto --notest + +alias install := sync +alias develop := sync + +# Sort imports +isort: + -pre-commit run reorder-python-imports --all-files # Run tests test: - pytest + pytest -q -n 4 --cov-report=html # Run all tests test-all: - tox + tox -p auto + +alias tox := test-all +alias t := test-all # Run lints lint: @@ -31,6 +48,13 @@ lint: mypy: mypy +# run the flask application +serve: + flask run + +alias s := serve +alias run := serve + # Build docs docs: cd docs && make html @@ -46,7 +70,7 @@ clean-docs: rm -rf docs/api # Clean aggressively -clean-all: clean +clean-all: clean clean-docs rm -rf .direnv rm -rf .venv rm -rf .tox diff --git a/scripts/check-dist.py b/scripts/check-dist.py index fef0c3d..f0021e4 100755 --- a/scripts/check-dist.py +++ b/scripts/check-dist.py @@ -1,99 +1,129 @@ #!/usr/bin/env python3 import contextlib +import dataclasses import os import subprocess +import sys +import tempfile from collections.abc import Iterator +from concurrent.futures import ThreadPoolExecutor +from concurrent.futures import wait from pathlib import Path import click -def run(*args: str) -> None: - cmd = " ".join(args) - click.echo("{} {}".format(click.style(">", fg="blue", bold=True), cmd)) +@dataclasses.dataclass +class VirtualEnv: + + path: Path + + def run(self, *args: object) -> None: + python = self.path / "bin" / "python" + run(python, *args) + - verbose = click.get_current_context().meta["verbose"] - process = subprocess.run(args, capture_output=(not verbose)) +def run(*args: object) -> None: + args = [str(arg) for arg in args] + cmd = " ".join(args) + ctx = click.get_current_context() + + if not ctx.meta.get("quiet", False): + click.echo("{} {}".format(click.style(">", fg="blue", bold=True), cmd)) + + verbose = ctx.meta.get("verbose", False) + process = subprocess.run( + args, + stdout=subprocess.PIPE if not verbose else None, + stderr=subprocess.STDOUT if not verbose else None, + check=False, + ) if process.returncode != 0: click.echo( "{} {} failed with returncode {}".format(click.style("!", fg="red", bold=True), cmd, process.returncode), err=True, ) - if process.stderr or process.stdout: - click.echo(process.stdout.decode()) - click.echo(process.stderr.decode(), err=True) + if process.stdout: + click.echo(process.stdout.decode(), err=True) raise click.ClickException(f"Command failed with return code {process.returncode}") -def python(venv: Path, *args: str) -> None: - pybinary = venv / "bin" / "python" - run(str(pybinary), *args) +BUILD_COMMAND_ARG = { + "sdist": "-s", + "wheel": "-w", +} + +BUILD_ARTIFACT_PATTERN = { + "sdist": "*.tar.gz", + "wheel": "*.whl", +} -def dist(location: Path, pattern: str) -> Path: +def find_dist(location: Path, pattern: str) -> Path: candidates = sorted(location.glob(pattern), key=lambda p: p.stat().st_mtime, reverse=True) if not candidates: - raise click.ClickException("No sdist found") + raise click.ClickException(f"No {pattern} found") return candidates[0] -def clean(package: str) -> None: - run("rm", "-rf", f"src/{package}/assets/*") - run("rm", "-rf", "dist") - - @contextlib.contextmanager -def virtualenv(root: Path, name: str) -> Iterator[Path]: +def virtualenv(root: Path, name: str) -> Iterator[VirtualEnv]: + """Create a virtualenv and yield the path to it.""" run("python", "-m", "venv", str(root / name)) - yield root / name + yield VirtualEnv(root / name) run("rm", "-rf", str(root / name)) -def check(venv: Path, package: str, assets: bool = False) -> None: - python(venv, "-c", f"import {package}; print({package}.__version__)") - - if assets: - python(venv, "-c", f"import {package}.assets; {package}.assets.check_dist()") +def check_dist(ctx: click.Context, package: str, dist: str, assets: bool = False) -> None: - python(venv, "-m", "pip", "install", "twine") - python(venv, "-m", "twine", "check", f"dist/{package}-*") + with ctx.scope(), tempfile.TemporaryDirectory() as tmp_directory: + tmpdir = Path(tmp_directory) + distdir = tmpdir / "dist" + run(sys.executable, "-m", "build", BUILD_COMMAND_ARG[dist], ".", "--outdir", distdir) + with virtualenv(tmpdir, "venv-dist") as venv: + venv.run("-m", "pip", "install", "--upgrade", "pip") + sdist = find_dist(distdir, BUILD_ARTIFACT_PATTERN[dist]) + venv.run("-m", "pip", "install", str(sdist)) -def sdist(package: str, assets: bool = False) -> None: - clean(package=package) - run("python", "-m", "build", "-s", ".") - with virtualenv(Path("dist"), "venv-sdist") as venv: - python(venv, "-m", "pip", "install", "--upgrade", "pip") - sdist = dist(Path("dist/"), "*.tar.gz") - python(venv, "-m", "pip", "install", str(sdist)) - check(venv, package, assets=assets) - click.secho("sdist built and installed successfully", fg="green", bold=True) + venv.run("-c", f"import {package}; print({package}.__version__)") + if assets: + venv.run("-c", f"import {package}.assets; {package}.assets.check_dist()") + with virtualenv(tmpdir, "venv-twine") as venv: + venv.run("-m", "pip", "install", "twine") + venv.run("-m", "twine", "check", sdist) -def wheel(package: str, assets: bool = False) -> None: - clean(package=package) - run("python", "-m", "build", "-w", ".") - with virtualenv(Path("dist"), "venv-wheel") as venv: - wheel = dist(Path("dist/"), "*.whl") - python(venv, "-m", "pip", "install", str(wheel)) - check(venv, package, assets=assets) - click.secho("wheel built and installed successfully", fg="green", bold=True) + click.secho(f"{dist} built and installed successfully", fg="green", bold=True) @click.command() @click.option("-v", "--verbose", is_flag=True, help="Enable verbose output") +@click.option("-q", "--quiet", is_flag=True, help="Enable quiet output") @click.option("-a", "--assets", is_flag=True, help="Check assets") -@click.argument("package", type=str) +@click.option("-t", "--timeout", default=60.0, help="Timeout for checking distribution") +@click.argument("toxinidir", type=str, required=True) @click.pass_context -def main(ctx: click.Context, package: str, verbose: bool, assets: bool) -> None: +def main(ctx: click.Context, toxinidir: str, verbose: bool, quiet: bool, assets: bool, timeout: float) -> None: """Check distribution for package""" if os.environ.get("CI") == "true": verbose = True + ctx.meta["quiet"] = quiet ctx.meta["verbose"] = verbose - sdist(package, assets=assets) - wheel(package, assets=assets) + + package = Path(toxinidir).name + click.secho(f"Checking distribution for {package}", bold=True) + + with ThreadPoolExecutor() as executor: + sdist = executor.submit(check_dist, ctx, package, "sdist", assets=assets) + wheel = executor.submit(check_dist, ctx, package, "wheel", assets=assets) + + done, _ = wait([sdist, wheel], return_when="ALL_COMPLETED", timeout=timeout) + + for future in done: + future.result() if __name__ == "__main__": diff --git a/scripts/check-minimal.py b/scripts/check-minimal.py new file mode 100644 index 0000000..73fab71 --- /dev/null +++ b/scripts/check-minimal.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 +import pathlib +import subprocess +import sys + + +def check_minimal(): + """Check if the package can be imported.""" + project = pathlib.Path(__file__).parent.name + print(f"Checking minimal for project: {project}") + + subprocess.run([sys.executable, "-c", f"import {project}"], check=True) + + +if __name__ == "__main__": + check_minimal() diff --git a/tests/nav/test_render.py b/tests/nav/test_render.py index 0176f8b..2015f17 100644 --- a/tests/nav/test_render.py +++ b/tests/nav/test_render.py @@ -40,7 +40,7 @@ def test_nav_alignment(alignment: core.NavAlignment, cls: str) -> None: source = render(nav) expected = f""" -