SIGN IN SIGN UP

Bump `serious_python` to 4.6.0; re-pin python-build to 20260902 (#6810)

* Bump serious_python to 4.6.0; re-pin python-build to 20260902

Bundled Python moves to 3.12.14 / 3.13.15 / 3.14.7 and Pyodide for 3.14 to
314.0.6. All three CPython micros are security releases (html.parser DoS
gh-153030, ElementTree XPath gh-152674); 3.12.14 additionally bundles libexpat
2.8.3 for CVE-2026-72522, which 3.13.15 and 3.14.7 do not yet carry.

Two pins have to move together, as the comment on PYTHON_BUILD_RELEASE_DATE
says: serious_python 4.6.0 in the build template, and the python-build release
this repo fetches its manifest from. serious_python 4.6.0 tracks 20260902 as
its own pythonReleaseDate, so the two stay in sync.

Verified against the live release rather than assumed -- with a cold cache the
pin resolves to 3.12.14 / 3.13.15 / 3.14.7, default 3.14, and
resolve_python_version returns 3.14.7 / 3.13.15 / 3.12.14 for the default and
each explicit short version. All six serious_python_* packages are published on
pub.dev at 4.6.0.

Both web paths have real artifacts for Pyodide 314.0.6: jsdelivr serves
v314.0.6/full/pyodide.mjs (CDN mode) and the pyodide-core-314.0.6.tar.bz2
release asset exists (--no-cdn).

Docs tables move with the pin. Two of the cells they update had drifted well
before this bump: the 3.14 Pyodide value read 314.0.0 in both the publish
matrix and the static-website note, and the same stale number appeared in
patch_index.py's comment enumerating the supported Pyodide versions -- that
comment's claim (all of them ship pyodide.mjs) still holds.

Left alone deliberately:

  * tests/test_python_versions.py's fixture manifest. It is synthetic -- it
    carries a fabricated 3.15 pre-release row to exercise the opt-in path -- and
    the tests run fully offline against it. Tracking the real manifest would
    couple them to a live release for no gain.
  * The v0.86 release blog post and the v0-86-0 breaking-change note. Both
    record what was true at the time.

* Changelog entry for the serious_python 4.6.0 bump

* Don't crash when a component has no body yet

A component that re-renders is patched to a null body first and receives
its new body in a follow-up message. For the gap between the two it has
no body at all - normally invisible, but if the client draws a frame in
between, the parent asks for its children and unwrapComponent() returns
that null through a non-nullable Control return type, throwing
"type 'Null' is not a subtype of type 'Control'" and taking down the
widget tree.

unwrapComponent() now returns Control?. children() drops those nulls and
parseControlWidget() returns null for them, so a body-less component
renders nothing for that frame and appears once its body arrives.

Timing-dependent, which is what made it look like flaky CI: it surfaced
as a widget exception during route changes that rebuild a view's
contents, failing examples/apps test_runtime_routes with every one of
its own assertions passing.
F
Feodor Fitsner committed
2cc3c12fce33fc03cfecf61786e99d5e887cf0f6
Parent: a7dbd1b
Committed by GitHub <noreply@github.com> on 9/3/2026, 7:29:10 PM