Created
September 17, 2025 16:12
-
-
Save MattOates/382a758c5cf60aa636cc6ad704452984 to your computer and use it in GitHub Desktop.
A template standalone script that has some testing and can be shipped as a single file readily
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
| #!/usr/bin/env -S uv run --script | |
| # /// script | |
| # requires-python = ">=3.11" | |
| # dependencies = ["typer", "pytest"] # keep this minimal; add libs you actually use | |
| # /// | |
| # -*- coding: utf-8 -*- | |
| from pathlib import Path | |
| import typer | |
| def transform(xs: list[int]) -> list[int]: | |
| """ | |
| Example pure function with doctests. | |
| >>> transform([1, 2, 3, 4]) | |
| [1, 4, 3, 16] | |
| Edge cases: | |
| >>> transform([]) | |
| [] | |
| """ | |
| return [x * x if x % 2 == 0 else x for x in xs] | |
| def parse_csv_line(line: str) -> list[str]: | |
| """ | |
| Extremely basic CSV split (placeholder). | |
| >>> parse_csv_line("a,b,c") | |
| ['a', 'b', 'c'] | |
| >>> parse_csv_line('a, "b", c') # doctest: +ELLIPSIS | |
| ['a', '"b"', 'c'] | |
| """ | |
| return [p.strip() for p in line.split(",")] | |
| app = typer.Typer(add_completion=False, no_args_is_help=True) | |
| # CLI commands | |
| @app.command() | |
| def run( | |
| input_path: Path = typer.Argument( | |
| ..., help="Input file (placeholder for real CLI)" | |
| ), | |
| output_path: Path | None = typer.Option(None, "--out", help="Output path"), | |
| ) -> None: | |
| """ | |
| Example command: read first line, transform a toy payload, print result. | |
| Replace this with your real entrypoint; keep it thin and call pure functions above. | |
| """ | |
| line = input_path.read_text().splitlines()[0] if input_path.exists() else "" | |
| parts = parse_csv_line(line) | |
| nums = [int(p) for p in parts if p.isdigit()] | |
| result = transform(nums) | |
| if output_path: | |
| output_path.write_text(",".join(map(str, result)) + "\n") | |
| else: | |
| print(result) | |
| # Entrypoint | |
| if __name__ == "__main__": | |
| app() | |
| # Tests | |
| # Note: this self-test command is optional; remove if you don't want it | |
| @app.command("self-test") | |
| def self_test( | |
| pytest_args: list[str] = typer.Argument(None), | |
| doctest_verbose: bool = typer.Option(False, "--doctest-verbose"), | |
| ) -> None: | |
| """ | |
| Run doctest first, then pytest. | |
| If this script isn't a *.py file, create a temporary directory containing | |
| a *.py *symlink* to this file, run pytest against that symlink path, | |
| and then clean it up. | |
| """ | |
| import doctest | |
| import sys | |
| # 1) Run doctests on the current module | |
| flags = doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE | |
| fails, _ = doctest.testmod( | |
| sys.modules[__name__], optionflags=flags, verbose=doctest_verbose | |
| ) | |
| # 2) If pytest isn't available, just report doctest result | |
| try: | |
| import pytest # type: ignore | |
| except Exception: | |
| raise typer.Exit(code=1 if fails else 0) | |
| this_file = Path(__file__).resolve() | |
| exit_code = 1 if fails else 0 | |
| # 3) Choose target for pytest (real .py file, or temp symlink) | |
| target: Path | |
| tmpdir = None | |
| try: | |
| if this_file.suffix == ".py": | |
| target = this_file | |
| else: | |
| import os | |
| import tempfile | |
| tmpdir = tempfile.TemporaryDirectory() | |
| link = Path(tmpdir.name) / (this_file.name + ".py") | |
| # create symlink pointing to the current file | |
| try: | |
| os.symlink(this_file, link) | |
| # on Windows, symlink may require admin rights; fall back to copy | |
| except OSError: | |
| import shutil | |
| shutil.copy2(this_file, link) | |
| target = link | |
| # 4) Run pytest against the target | |
| code = pytest.main([str(target), *(pytest_args or [])]) | |
| if code != 0: | |
| exit_code = code | |
| finally: | |
| if tmpdir is not None: | |
| tmpdir.cleanup() | |
| raise typer.Exit(code=exit_code) | |
| # Optional: bigger doctest scenarios live here if they don't read well in docstrings, | |
| # prefer just doing pytest functions though | |
| __test__ = { | |
| "round_trip_example": r""" | |
| >>> xs = [0, 1, 2] | |
| >>> transform(xs) | |
| [0, 1, 4] | |
| """ | |
| } | |
| def test_transform_squares_evens_only(): | |
| assert transform([1, 2, 3, 4]) == [1, 4, 3, 16] | |
| def test_parse_csv_line_basic(): | |
| assert parse_csv_line("x,y") == ["x", "y"] | |
| def test_cli_smoke(tmp_path: Path): | |
| from typer.testing import CliRunner | |
| runner = CliRunner() | |
| p = tmp_path / "in.txt" | |
| p.write_text("1, 2, 3\n") | |
| result = runner.invoke(app, ["run", str(p)]) | |
| assert result.exit_code == 0, result.stderr | |
| assert result.stdout.strip() == "[1, 4, 3]" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment