Ushka

API reference

ushka.guard.rewrite

Compile-time rewriting that keeps the Untrusted mark where CPython drops it.

CPython builds f-strings, "...".format(), "..." % (a, b) and sep.join(items) in C, without asking the operands, so a plain str subclass cannot propagate through them. Since Ushka loads the files in routes/ itself, it can compile them through this module, which rewrites exactly those expressions into calls to small helpers that propagate the mark. Code outside the rewritten modules (the stdlib, third-party packages) is untouched, so it keeps its exact semantics.

Rewrites (only inside the compiled module):

============================== =========================================== source becomes ============================== =========================================== f"a{x!r:>10}b" G.fstr(("a", G.fmt(x, "r", ">10"), "b")) obj.format(*a, **k) G.format(obj, *a, **k) obj.join(items) G.join(obj, items) left % right G.mod(left, right) str(x) G.str_(str, x) ============================== ===========================================

Every helper falls back to the original operation when the receiver is not a str (bytes.join, int % int, a shadowed str...), so behaviour only changes in one way: results built from Untrusted values stay Untrusted. t-strings (t"...") are left alone; they need no help.

def rewrite_tree(tree: ast.Module) -> ast.Module

Applies the taint-preserving rewrite to a parsed module.

def compile_source(source: str, filename: str) -> types.CodeType

Parses, rewrites and compiles module source code.

def exec_file(module: types.ModuleType, path: str | Path) -> None

Executes the file at path as module, with the rewrite applied.

def install(prefixes: Iterable[str]) -> None

Applies the rewrite to every module imported under prefixes from now on.

Call it before importing your own code, e.g. at the top of app.py:

from ushka.guard import rewrite
rewrite.install(["myapp"])
from myapp import views

Modules that are already imported are not affected. Only your own packages should be listed; the stdlib and dependencies keep running unmodified.

def uninstall() -> None

Removes the import hook installed by install().