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]")

...