paulw.tokyo

Standalone python script with uv

Uv is a neat package manager, which took off last year in python land. Being written in Rust™ immediately gives it +100 street credibility.

As it supports reading inline script metadata (see PEP 723) it's easy to write scripts that specify their own dependencies, in a single file.

You still need to install uv itself though, and while it's simple it's still an extra step, which is exactly one too many. So here is how I inline that as well into the script:

#!/usr/bin/env sh
#!/usr/bin/env -S uv run --script --quiet
# /// script
# requires-python = "~=3.12"
# dependencies = [
#     "numpy==2.1.1",
# ]
# ///

""":"
which uv >/dev/null \
    || curl -LsSf https://astral.sh/uv/install.sh | sh \
    && tail -n +3 $0 | $(head -n 2 $0 | tail -n 1 | cut -c 3-) - "$@"
exit $?
":"""

from fractions import Fraction as F
import sys
import numpy as np

print(f"""Python script running with:
{sys.executable}
{sys.argv[1:]}
{np.__version__=}""")

# Idiomatic python, print numbers 0 to 96
print(
    *map(lambda x: x[0]+x[1], zip((z := f"{F(1, 9801):.192f}")[2::2], z[3::2]))
)

The trick is simple, we set the shebang to be our shell #!/usr/bin/env sh, meaning that the rest of the file will be interpreted as a shell script. Conveniently, both python and POSIX shells treat lines starting with # as comments.

The shell interprets """:" as an empty pair of quotes, then a quoted no-op : command and symmetrically for ":""". In python, the whole section in between triple quotes is an unassigned string.

The script part commented a bit is simply:

# Check if `uv` is available
if $(which uv >/dev/null); then
    # If yes, nothing to do
else
    # Otherwise download and install uv
    curl -LsSf https://astral.sh/uv/install.sh | sh 
fi

# Then remove the first two lines from the current file and pipe that
# to the command from the second shebang (here `uv run --script --quiet`), with any given arguments
tail -n +3 $0 | $(head -n 2 $0 | tail -n 1 | cut -c 3-) - "$@"

# Exit now –with whatever the python script returned– to avoid trying to execute the rest of the file
exit $?

Bonus: to remove the shell part after checking/installing uv, use this instead:

which uv >/dev/null \
    || curl -LsSf https://astral.sh/uv/install.sh | sh \
    && tail -n +2 $0 | awk '/^.?"":"$/,/^":"".?$/ {next} {print}' > /tmp/toswap.txt \
    && mv /tmp/toswap.txt $0 \
    && chmod +x $0 \
    && exec $0 $@
exit $?

#overkill #programming #python #shell