Background #
Cog #
I’ve been dying to have some fun with cog
ever since I
overheard Ned Batchelder (@nedbat
) talking about it with someone else.
Here’s its own description:
Cog is a content generation tool. It lets you use small bits of Python code in otherwise static files to generate whatever text you need.
I think the best and most fun example of how cog works is a hidden gem in the cog docs.
Cog embeds the the CLI --help
in the docs by using cog
to run cog -h
and dump the output (into the file).
This is poetry (no, not the Python tool). This is beautiful (not the soup).
Single-file scripts #
Also, if you haven’t heard of or tried them yet, the Python packaging ecosystem has a neat way of taking a Python script’s dependency specifier and “inlining” it at the top of a file in a comment TOML (not a TOML comment), making a “single-file script”.
The result is something that can be uv run
or pipx run
and is spiritually a Python “single-file executable”.
This is also beautiful and amazing.
Problem #
Single-file scripts lack a way to pin transitive dependencies.
While uv
supports this with uv lock --script
, it creates a separate lock file, defeating the whole “single-file” concept.
(Really I want to fully embed the lockfile, but pinned transitive deps are “good enough” for now).
Solution #
Use cog
to generate to pin the dependencies!
# /// script
# dependencies = [
# # [[[cog
# # import subprocess
# # DEPENDENCIES = [
# # "cyclopts",
# # "httpx",
# # ]
# # cog.outl(
# # "\n".join(
# # f'# "{line}",' for line in
# # subprocess.check_output(
# # "uv pip compile --no-annotate --no-header -",
# # shell=True,
# # text=True,
# # input="\n".join(DEPENDENCIES)
# # ).splitlines()
# # )
# # )
# # ]]]
# "anyio==4.9.0",
# "<the others omitted for brevity>",
# # [[[end]]]
# ]
# ///
import cyclopts
import httpx
# Script code here...
And now you can update (and pin) dependencies anytime with:
uvx --from cogapp cog -r <path>
Caveat: When compared with a full lockfile we still lack features such as hash support (if you’re into supply-chain security) and friends. However, incremental improvement is still improvement.
Bonus: Self-relocking #
This also means your single-file script (usually a CLI app) can now have a self relock
command!
# <same preamble>
import cyclopts
import rich
import subprocess
app = cyclopts.App()
self_app = cyclopts.App(name="self")
app.command(self_app)
@self_app.command()
def relock():
"""Update this script's pinned dependencies."""
subprocess.check_call(f"uvx --from cogapp cog -r {__file__}", shell=True)
rich.print("[green]Relocked successfully[/green]")
...