Skip to main content
Metaist

pythonoid: Emulating the Python CLI in Python

How much of the Python CLI can you emulate in Python?

When I was a kid, I was fascinated with tools that could build themselves (hence, "Metaist"). I once spent an entire week trying to make the QBasic interface (blue screen, menu bars) within QBasic. Like you'd press F5 to run the program and you'd end up in a blank QBasic editor screen. I learned a lot about drawing on the screen and capturing key presses.

Recently, while I was adding self-updating to cosmofy, I needed to intercept command-line arguments destined for python. The idea is that if you call the Cosmopolitan app with --self-update, it will check for updates (e.g., on GitHub) and replace itself with the latest version. Otherwise, it should continue as normal.

But how much of the Python Command-Line Interface can you implement in Python? Turns out, quite a bit.

Let's take a look at the usage:

python [-bBdEhiIOPqRsSuvVWx?] [-c command | -m module-name | script | - ] [args]

The next tricky bit is emulating locals to make sure that they look the same as they do with native python:

    if isinstance(__builtins__, dict):
        loader = __builtins__["__loader__"]
    else:  # pragma: no cover
        # During testing, __builtins__ is a dict.
        loader = __builtins__.__loader__

    local: Dict[str, object] = {
        "__name__": "__main__",
        "__doc__": None,
        "__package__": None,
        "__loader__": loader,
        "__spec__": None,
        "__annotations__": {},
        "__builtins__": __builtins__,
    }

We also manipulate exceptions to make them look the same as they do with the REPL:

    sys.argv = args.argv
    code = 0
    try:
        if args.c:  # execute in the context of the locals
            args.q = True
            exec(args.c, local, local)
        elif args.m:
            args.q = True
            runpy.run_module(args.m, local, "__main__", alter_sys=True)
        elif args.script:
            args.q = True
            local["__loader__"] = SourceFileLoader
            runpy.run_path(args.script, local, "__main__")
    except Exception as e:
        code = 1
        # NOTE: We skip the calling frame to emulate the CLI better.
        tb = sys.exc_info()[2]
        tb_next = tb.tb_next if tb else tb
        print("".join(traceback.format_exception(e.__class__, e, tb_next)), end="")

Putting it all together, you get pythonoid.py.

pythonoid also has some nice functions for compiling python into a bytearray that cosmofy can use to write directly into the bundle.