-
Notifications
You must be signed in to change notification settings - Fork 0
Prescribe the uv --script shebang and test it #38
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,135 @@ | ||
| """Validate the PEP 723 uv script shebang standard. | ||
|
|
||
| ``#!/usr/bin/env -S uv run python`` executes the interpreter directly and | ||
| silently ignores the PEP 723 inline metadata block (``# /// script`` ... | ||
| ``# ///``), so a script invoked directly (``./script.py``) fails at import | ||
| time because its declared dependencies were never installed. The correct | ||
| shebang is ``#!/usr/bin/env -S uv run --script``, which reads the metadata | ||
| block and installs the declared dependencies before execution. | ||
|
|
||
| This module guards against the broken shebang reappearing anywhere in the | ||
| repository or the Copier template tree, and behaviourally proves that the | ||
| prescribed shebang works as intended. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import shutil | ||
| import stat | ||
| import subprocess | ||
| from pathlib import Path | ||
|
|
||
| import pytest | ||
|
|
||
| REPOSITORY_ROOT = Path(__file__).resolve().parents[1] | ||
| BROKEN_SHEBANG = "#!/usr/bin/env -S uv run python" | ||
| CORRECT_SHEBANG = "#!/usr/bin/env -S uv run --script" | ||
| SCRIPT_BLOCK_MARKER = "# /// script" | ||
| EXCLUDED_DIRECTORY_NAMES = {".git"} | ||
| LOOKAHEAD_LINES = 5 | ||
|
|
||
|
|
||
| def _iter_repository_files() -> list[Path]: | ||
| """Return every tracked-style file under the repository, excluding VCS internals.""" | ||
| files: list[Path] = [] | ||
| for path in REPOSITORY_ROOT.rglob("*"): | ||
| if not path.is_file(): | ||
| continue | ||
| if EXCLUDED_DIRECTORY_NAMES & set(path.relative_to(REPOSITORY_ROOT).parts): | ||
| continue | ||
| files.append(path) | ||
| return files | ||
|
|
||
|
|
||
| def _find_broken_shebang_pep723_scripts() -> list[str]: | ||
| """Return relative paths of files whose broken shebang heads a PEP 723 block. | ||
|
|
||
| A violation is a line exactly matching ``BROKEN_SHEBANG`` with a | ||
| ``# /// script`` marker within the next few lines, which indicates the | ||
| shebang introduces a PEP 723 inline-metadata script rather than merely | ||
| appearing in prose or an unrelated command example. | ||
| """ | ||
| violations: list[str] = [] | ||
| for path in _iter_repository_files(): | ||
| try: | ||
| text = path.read_text(encoding="utf-8") | ||
| except (UnicodeDecodeError, OSError): | ||
| continue | ||
| lines = text.splitlines() | ||
| for index, line in enumerate(lines): | ||
| if line.strip() != BROKEN_SHEBANG: | ||
| continue | ||
| lookahead = lines[index + 1 : index + 1 + LOOKAHEAD_LINES] | ||
| if any(SCRIPT_BLOCK_MARKER in candidate for candidate in lookahead): | ||
| violations.append(str(path.relative_to(REPOSITORY_ROOT))) | ||
| return violations | ||
|
|
||
|
|
||
| def test_no_broken_uv_shebang_heads_a_pep723_script() -> None: | ||
| """No file in the repository or template tree ships the broken shebang. | ||
|
|
||
| Returns | ||
| ------- | ||
| None | ||
| The test passes when every PEP 723 script block is introduced by | ||
| ``#!/usr/bin/env -S uv run --script`` rather than the broken | ||
| ``#!/usr/bin/env -S uv run python`` form, which silently ignores the | ||
| metadata block on direct execution. | ||
| """ | ||
| violations = _find_broken_shebang_pep723_scripts() | ||
| assert violations == [], ( | ||
| "expected no PEP 723 script to be headed by the broken " | ||
| f"'{BROKEN_SHEBANG}' shebang; offending files: {violations}" | ||
| ) | ||
|
|
||
|
|
||
| def test_correct_uv_shebang_installs_declared_dependency(tmp_path: Path) -> None: | ||
| """A script with the prescribed shebang installs and imports its dependency. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| tmp_path : Path | ||
| Temporary directory used to host the executable script under test. | ||
|
|
||
| Returns | ||
| ------- | ||
| None | ||
| The test passes when a directly executed script using | ||
| ``#!/usr/bin/env -S uv run --script`` installs its declared | ||
| dependency, imports it successfully, and exits with code ``0``. | ||
| """ | ||
| uv_executable = shutil.which("uv") | ||
| if uv_executable is None: | ||
| pytest.skip("uv is unavailable to exercise the shebang behaviourally") | ||
|
|
||
| script_path = tmp_path / "shebang_probe.py" | ||
| script_source = ( | ||
| f"{CORRECT_SHEBANG}\n" | ||
| "# /// script\n" | ||
| '# requires-python = ">=3.13"\n' | ||
| '# dependencies = ["packaging"]\n' | ||
| "# ///\n" | ||
| "\n" | ||
| "import packaging\n" | ||
| "\n" | ||
| 'print(f"packaging-ok:{packaging.__version__}")\n' | ||
| ) | ||
| script_path.write_text(script_source, encoding="utf-8") | ||
| script_path.chmod(script_path.stat().st_mode | stat.S_IEXEC) | ||
|
|
||
| result = subprocess.run( # noqa: S603 - argv is the freshly written temp script. | ||
| [str(script_path)], | ||
| capture_output=True, | ||
| text=True, | ||
| timeout=120, | ||
| check=False, | ||
| ) | ||
|
|
||
| assert result.returncode == 0, ( | ||
| "expected the correctly shebanged script to run and exit cleanly:\n" | ||
| f"stdout: {result.stdout}\nstderr: {result.stderr}" | ||
| ) | ||
| assert "packaging-ok:" in result.stdout, ( | ||
| "expected the script to successfully import its declared dependency:\n" | ||
| f"stdout: {result.stdout}\nstderr: {result.stderr}" | ||
| ) | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In network-restricted CI or local runs where
uvis installed but PyPI is unavailable, this test fails before it can validate the shebang: the temp script declarespackaging, souv run --scripthas to resolve the package from the default PyPI index at execution time. I reproducedpython -m pytest tests/test_scripting_shebang.py -qfailing withFailed to fetch: https://pypi.org/simple/packaging/; use a local path dependency or skip when dependency resolution cannot run rather than making the parent test suite depend on live PyPI.Useful? React with 👍 / 👎.