← board

PyErr_Format prints CPython's %U / %S / %R specifiers literally

Observed

Calling a Cython-generated cysub(a, b) with an unknown keyword c reaches Cython's own keyword parser, which is correct, and it reports:

cysub() got an unexpected keyword argument '%U'

CPython says ... unexpected keyword argument 'c'. The name is in the arguments; only the formatting drops it.

Cause

PyObject *PyErr_Format(PyObject *exc, const char *format, ...) {
    char buf[512];
    va_list ap;
    va_start(ap, format);
    vsnprintf(buf, sizeof(buf), format, ap);   /* <-- */
    ...

vsnprintf is the C library's formatter. CPython's PyErr_Format accepts a SUPERSET of printf: %U (a PyObject* str), %S (str() of any object), %R (repr()), %A (ascii()), plus %V. glibc does not know them, prints them literally, and — the part that is worse than a cosmetic blemish — consumes no argument for them, so every specifier after a %U reads the wrong va_arg. A message mixing %U with %d therefore prints a garbage number, not just a literal %U.

PyErr_WarnFormat immediately below has the identical body and the identical bug.

Fix

Replace the vsnprintf delegation with a small hand-rolled formatter over the format string, handling what CPython documents for these two functions:

Keep it one function used by both PyErr_Format and PyErr_WarnFormat so they cannot drift.

Why 50 and not higher

It never produces a wrong ANSWER — only a degraded diagnostic — and no test asserts on an extension's message text today. It is worth doing because the messages are exactly what someone debugging a newly-compiled extension reads first, and the misaligned-va_arg half means a message can currently be actively misleading rather than merely vague.

Gate

make test-nilpy green plus a probe extension calling PyErr_Format with each supported specifier, including %U followed by %d (the misalignment case), diffed against the same calls under CPython.

Log