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]
-h
: just print the subset of the--help
message that we implement (since it's not the full thing)-i
: go into the REPL after-c
,-m
, or<script>
usingcode.interact
-q
: don't print headers in the REPL-V
: printsys.version
information (also handle-VV
for printing a little bit more version info)-c
: run a command usingexec
-m
: run a module usingrunpy.run_module
<script>
: run a script usingrunpy.run_path
-
: read fromstdin
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.