Skip to content

Commit b1fc58b

Browse files
committed
Update development tools to a common baseline
Common baseline provides tox, justfile, and check scripts.
1 parent 42ecc74 commit b1fc58b

File tree

6 files changed

+143
-66
lines changed

6 files changed

+143
-66
lines changed

.flake8

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
[flake8]
22
select = B, E, F, W, B9, ISC
33
ignore =
4+
B902
45
E203
56
E402
67
E501
78
E704
9+
E711
10+
E712
811
E722
912
W503
1013
W504
11-
E712,E711
12-
B902
1314
max-line-length = 120
14-
min_python_version = 3.11
15-
exclude = src/dominate-stubs
15+
min_python_version = 3.11.0

justfile

+32-8
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,44 @@
1+
# Common Flask project tasks
12

23
# Set up the virtual environment based on the direnv convention
34
# https://direnv.net/docs/legacy.html#virtualenv
4-
virtual_env := justfile_directory() / ".direnv/python-3.11/bin"
5+
python_version := env('PYTHON_VERSION', "3.12")
6+
virtual_env := justfile_directory() / ".direnv/python-$python_version/bin"
57
export PATH := virtual_env + ":" + env('PATH')
8+
export REQUIREMENTS_TXT := env('REQUIREMENTS', '')
69

710
[private]
811
prepare:
912
pip install --quiet --upgrade pip
10-
pip install --quiet pip-tools pip-compile-multi
13+
pip install --quiet -r requirements/pip-tools.txt
1114

15+
# lock the requirements files
16+
compile: prepare
17+
pip-compile-multi --use-cache --backtracking
18+
19+
# Install dependencies
1220
sync: prepare
13-
pip-compile-multi --use-cache
1421
pip-sync requirements/dev.txt
15-
pip install -e .
16-
tox --notest
22+
[[ -f requirements/local.txt ]] && pip install -r requirements/local.txt
23+
tox -p auto --notest
24+
25+
alias install := sync
26+
alias develop := sync
27+
28+
# Sort imports
29+
isort:
30+
-pre-commit run reorder-python-imports --all-files
1731

1832
# Run tests
1933
test:
20-
pytest
34+
pytest -q -n 4 --cov-report=html
2135

2236
# Run all tests
2337
test-all:
24-
tox
38+
tox -p auto
39+
40+
alias tox := test-all
41+
alias t := test-all
2542

2643
# Run lints
2744
lint:
@@ -31,6 +48,13 @@ lint:
3148
mypy:
3249
mypy
3350

51+
# run the flask application
52+
serve:
53+
flask run
54+
55+
alias s := serve
56+
alias run := serve
57+
3458
# Build docs
3559
docs:
3660
cd docs && make html
@@ -46,7 +70,7 @@ clean-docs:
4670
rm -rf docs/api
4771

4872
# Clean aggressively
49-
clean-all: clean
73+
clean-all: clean clean-docs
5074
rm -rf .direnv
5175
rm -rf .venv
5276
rm -rf .tox

scripts/check-dist.py

+78-48
Original file line numberDiff line numberDiff line change
@@ -1,99 +1,129 @@
11
#!/usr/bin/env python3
22
import contextlib
3+
import dataclasses
34
import os
45
import subprocess
6+
import sys
7+
import tempfile
58
from collections.abc import Iterator
9+
from concurrent.futures import ThreadPoolExecutor
10+
from concurrent.futures import wait
611
from pathlib import Path
712

813
import click
914

1015

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+
1425

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+
)
1741
if process.returncode != 0:
1842
click.echo(
1943
"{} {} failed with returncode {}".format(click.style("!", fg="red", bold=True), cmd, process.returncode),
2044
err=True,
2145
)
2246

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)
2649
raise click.ClickException(f"Command failed with return code {process.returncode}")
2750

2851

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+
}
3261

3362

34-
def dist(location: Path, pattern: str) -> Path:
63+
def find_dist(location: Path, pattern: str) -> Path:
3564
candidates = sorted(location.glob(pattern), key=lambda p: p.stat().st_mtime, reverse=True)
3665
if not candidates:
37-
raise click.ClickException("No sdist found")
66+
raise click.ClickException(f"No {pattern} found")
3867
return candidates[0]
3968

4069

41-
def clean(package: str) -> None:
42-
run("rm", "-rf", f"src/{package}/assets/*")
43-
run("rm", "-rf", "dist")
44-
45-
4670
@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."""
4873
run("python", "-m", "venv", str(root / name))
49-
yield root / name
74+
yield VirtualEnv(root / name)
5075
run("rm", "-rf", str(root / name))
5176

5277

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:
5879

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)
6184

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))
6289

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()")
7293

94+
with virtualenv(tmpdir, "venv-twine") as venv:
95+
venv.run("-m", "pip", "install", "twine")
96+
venv.run("-m", "twine", "check", sdist)
7397

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)
8299

83100

84101
@click.command()
85102
@click.option("-v", "--verbose", is_flag=True, help="Enable verbose output")
103+
@click.option("-q", "--quiet", is_flag=True, help="Enable quiet output")
86104
@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)
88107
@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:
90109
"""Check distribution for package"""
91110
if os.environ.get("CI") == "true":
92111
verbose = True
93112

113+
ctx.meta["quiet"] = quiet
94114
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()
97127

98128

99129
if __name__ == "__main__":

scripts/check-minimal.py

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#!/usr/bin/env python3
2+
import pathlib
3+
import subprocess
4+
import sys
5+
6+
7+
def check_minimal():
8+
"""Check if the package can be imported."""
9+
project = pathlib.Path(__file__).parent.name
10+
print(f"Checking minimal for project: {project}")
11+
12+
subprocess.run([sys.executable, "-c", f"import {project}"], check=True)
13+
14+
15+
if __name__ == "__main__":
16+
check_minimal()

tests/nav/test_render.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def test_nav_alignment(alignment: core.NavAlignment, cls: str) -> None:
4040

4141
source = render(nav)
4242
expected = f"""
43-
<ul class='nav {cls if cls else str()}'>
43+
<ul class='nav {cls if cls else ''}'>
4444
<li class='nav-item'><span class='nav-link disabled' aria-disabled='true'>Text</span></li>
4545
</ul>"""
4646

tox.ini

+12-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[tox]
22
envlist =
3-
py311
4-
py312
3+
py3{11,12}
4+
coverage
55
style
66
typing
77
docs
@@ -11,7 +11,14 @@ skip_missing_interpreters = true
1111

1212
[testenv]
1313
deps = -r requirements/tests.txt
14-
commands = pytest -v --tb=short --basetemp={envtmpdir} {posargs}
14+
commands =
15+
pytest -v --tb=short --basetemp={envtmpdir} {posargs}
16+
17+
[testenv:coverage]
18+
depends = py3{11,12}
19+
deps = -r requirements/tests.txt
20+
commands =
21+
coverage report --fail-under=90 --skip-covered
1522

1623
[testenv:style]
1724
deps = pre-commit
@@ -28,11 +35,11 @@ commands = sphinx-build -W -b html -d {envtmpdir}/doctrees {toxinidir}/docs {env
2835

2936
[testenv:minimal]
3037
deps =
31-
commands = python -c 'import bootlace'
38+
commands = python {toxinidir}/scripts/check-minimal.py
3239

3340
[testenv:dist]
3441
deps =
3542
hatch
3643
build
3744
skip_install = true
38-
commands = python {toxinidir}/scripts/check-dist.py bootlace
45+
commands = python {toxinidir}/scripts/check-dist.py {toxinidir} {posargs:-q}

0 commit comments

Comments
 (0)