|
| 1 | +"""Validate the PEP 723 uv script shebang standard. |
| 2 | +
|
| 3 | +``#!/usr/bin/env -S uv run python`` executes the interpreter directly and |
| 4 | +silently ignores the PEP 723 inline metadata block (``# /// script`` ... |
| 5 | +``# ///``), so a script invoked directly (``./script.py``) fails at import |
| 6 | +time because its declared dependencies were never installed. The correct |
| 7 | +shebang is ``#!/usr/bin/env -S uv run --script``, which reads the metadata |
| 8 | +block and installs the declared dependencies before execution. |
| 9 | +
|
| 10 | +This module guards against the broken shebang reappearing anywhere in the |
| 11 | +repository or the Copier template tree, and behaviourally proves that the |
| 12 | +prescribed shebang works as intended. |
| 13 | +""" |
| 14 | + |
| 15 | +from __future__ import annotations |
| 16 | + |
| 17 | +import shutil |
| 18 | +import stat |
| 19 | +import subprocess |
| 20 | +from pathlib import Path |
| 21 | + |
| 22 | +import pytest |
| 23 | + |
| 24 | +REPOSITORY_ROOT = Path(__file__).resolve().parents[1] |
| 25 | +BROKEN_SHEBANG = "#!/usr/bin/env -S uv run python" |
| 26 | +CORRECT_SHEBANG = "#!/usr/bin/env -S uv run --script" |
| 27 | +SCRIPT_BLOCK_MARKER = "# /// script" |
| 28 | +EXCLUDED_DIRECTORY_NAMES = {".git"} |
| 29 | +LOOKAHEAD_LINES = 5 |
| 30 | + |
| 31 | + |
| 32 | +def _iter_repository_files() -> list[Path]: |
| 33 | + """Return every tracked-style file under the repository, excluding VCS internals.""" |
| 34 | + files: list[Path] = [] |
| 35 | + for path in REPOSITORY_ROOT.rglob("*"): |
| 36 | + if not path.is_file(): |
| 37 | + continue |
| 38 | + if EXCLUDED_DIRECTORY_NAMES & set(path.relative_to(REPOSITORY_ROOT).parts): |
| 39 | + continue |
| 40 | + files.append(path) |
| 41 | + return files |
| 42 | + |
| 43 | + |
| 44 | +def _find_broken_shebang_pep723_scripts() -> list[str]: |
| 45 | + """Return relative paths of files whose broken shebang heads a PEP 723 block. |
| 46 | +
|
| 47 | + A violation is a line exactly matching ``BROKEN_SHEBANG`` with a |
| 48 | + ``# /// script`` marker within the next few lines, which indicates the |
| 49 | + shebang introduces a PEP 723 inline-metadata script rather than merely |
| 50 | + appearing in prose or an unrelated command example. |
| 51 | + """ |
| 52 | + violations: list[str] = [] |
| 53 | + for path in _iter_repository_files(): |
| 54 | + try: |
| 55 | + text = path.read_text(encoding="utf-8") |
| 56 | + except (UnicodeDecodeError, OSError): |
| 57 | + continue |
| 58 | + lines = text.splitlines() |
| 59 | + for index, line in enumerate(lines): |
| 60 | + if line.strip() != BROKEN_SHEBANG: |
| 61 | + continue |
| 62 | + lookahead = lines[index + 1 : index + 1 + LOOKAHEAD_LINES] |
| 63 | + if any(SCRIPT_BLOCK_MARKER in candidate for candidate in lookahead): |
| 64 | + violations.append(str(path.relative_to(REPOSITORY_ROOT))) |
| 65 | + return violations |
| 66 | + |
| 67 | + |
| 68 | +def test_no_broken_uv_shebang_heads_a_pep723_script() -> None: |
| 69 | + """No file in the repository or template tree ships the broken shebang. |
| 70 | +
|
| 71 | + Returns |
| 72 | + ------- |
| 73 | + None |
| 74 | + The test passes when every PEP 723 script block is introduced by |
| 75 | + ``#!/usr/bin/env -S uv run --script`` rather than the broken |
| 76 | + ``#!/usr/bin/env -S uv run python`` form, which silently ignores the |
| 77 | + metadata block on direct execution. |
| 78 | + """ |
| 79 | + violations = _find_broken_shebang_pep723_scripts() |
| 80 | + assert violations == [], ( |
| 81 | + "expected no PEP 723 script to be headed by the broken " |
| 82 | + f"'{BROKEN_SHEBANG}' shebang; offending files: {violations}" |
| 83 | + ) |
| 84 | + |
| 85 | + |
| 86 | +def test_correct_uv_shebang_installs_declared_dependency(tmp_path: Path) -> None: |
| 87 | + """A script with the prescribed shebang installs and imports its dependency. |
| 88 | +
|
| 89 | + Parameters |
| 90 | + ---------- |
| 91 | + tmp_path : Path |
| 92 | + Temporary directory used to host the executable script under test. |
| 93 | +
|
| 94 | + Returns |
| 95 | + ------- |
| 96 | + None |
| 97 | + The test passes when a directly executed script using |
| 98 | + ``#!/usr/bin/env -S uv run --script`` installs its declared |
| 99 | + dependency, imports it successfully, and exits with code ``0``. |
| 100 | + """ |
| 101 | + uv_executable = shutil.which("uv") |
| 102 | + if uv_executable is None: |
| 103 | + pytest.skip("uv is unavailable to exercise the shebang behaviourally") |
| 104 | + |
| 105 | + script_path = tmp_path / "shebang_probe.py" |
| 106 | + script_source = ( |
| 107 | + f"{CORRECT_SHEBANG}\n" |
| 108 | + "# /// script\n" |
| 109 | + '# requires-python = ">=3.13"\n' |
| 110 | + '# dependencies = ["packaging"]\n' |
| 111 | + "# ///\n" |
| 112 | + "\n" |
| 113 | + "import packaging\n" |
| 114 | + "\n" |
| 115 | + 'print(f"packaging-ok:{packaging.__version__}")\n' |
| 116 | + ) |
| 117 | + script_path.write_text(script_source, encoding="utf-8") |
| 118 | + script_path.chmod(script_path.stat().st_mode | stat.S_IEXEC) |
| 119 | + |
| 120 | + result = subprocess.run( # noqa: S603 - argv is the freshly written temp script. |
| 121 | + [str(script_path)], |
| 122 | + capture_output=True, |
| 123 | + text=True, |
| 124 | + timeout=120, |
| 125 | + check=False, |
| 126 | + ) |
| 127 | + |
| 128 | + assert result.returncode == 0, ( |
| 129 | + "expected the correctly shebanged script to run and exit cleanly:\n" |
| 130 | + f"stdout: {result.stdout}\nstderr: {result.stderr}" |
| 131 | + ) |
| 132 | + assert "packaging-ok:" in result.stdout, ( |
| 133 | + "expected the script to successfully import its declared dependency:\n" |
| 134 | + f"stdout: {result.stdout}\nstderr: {result.stderr}" |
| 135 | + ) |
0 commit comments