Repository navigation
wasi-python 0.1.0
-
A coverage badge from CI (spec 124): the release workflow's new
coveragejob runs the fast tier serially under coverage.py (7.17.0, pinned in thedevgroup) against the set the build job made, and uploads the Cobertura report to Codecov with the repository'sCODECOV_TOKEN; the README carries Codecov's badge, andtest_readme_carries_the_codecov_badgeholds that it is this repository's.uv run python tests/harness/coverage.py --measuretakes the same measurement locally intobuild/coverage/. The-Vsentence and numpy's integer width are now held ondocs/reference/limitations.md, not the README. -
The wheel's LICENSE carries every component licence (spec 125): before each wheel build the release workflow runs
release/licences.py append, which rewrites the checkout's LICENSE as the Apache-2.0 text followed by everyLICENSES/*.txtof the set in name order, as Pillow's wheels do; the build job checks the wheel it built, and the judge's wheel check compares.dist-info/licenses/LICENSEwith the same text. FreeType's licence ships there, and the combined LICENSE's introduction gives FreeType's credit in words, so the README no longer carries it;release/licences.py restoreputs LICENSE back after a wheel built by hand;test_the_wheel_carries_every_component_licencereplaces the README credit check. -
A short README and a clean tree (spec 122): the README is cut to about 170 lines (what it is, a five-minute quickstart, what is safe by default, what runs, hosts, the stability promise, verifying a download, building, licences), every link absolute for PyPI. The generated API reference moves to
docs/reference/python-api.md(uv run python tests/harness/api.py --writewrites it,--pagenames another); the command's options are generated from its parser intodocs/reference/cli.mdbytests/harness/cli_doc.py, andtest_generated_pages_are_currentholds both pages current. The 560 known gaps and suite entries whose closer was the README, and pandas' excluded marker, now namedocs/reference/limitations.md, a new page of the platform's limits with a reason for each; six that named the questions file name the roadmap. The scans for document citations and embedder names read the README and every page underdocs/.pyproject.tomlsays "Development Status :: 4 - Beta", names all seven compiled packages, gains a Documentation URL and adocsdependency group (MkDocs 1.6.1, Material 9.7.7);mkdocs.ymland.github/workflows/docs.ymlbuild the site strictly on every pull request and push to main, and deploy it from a release tag.context/questions-for-the-owner.mdis deleted; its last paragraph moves tocontext/decisions.md. -
The documentation site (spec 123): a Diátaxis tree under
docs/(get started, how-to guides, reference, explanation, contributing), built with MkDocs and served athttps://claygendron.github.io/wasi_python/from each release tag. -
The release runs the pure-Python packages' own tests (spec 067): each whole suite before a publication, a smoke group per package on every run, for the 42 pure-Python packages the product ships; 39 test sources (each package's sdist, or its release tag's archive where the sdist carries no tests, and pypdf's sample files) are pinned and named in
MANIFEST, and the judge ties each to the bytes that ship. Four test-source patches are carried, each making one test-only import lazy or optional so the rest of its file runs:recipes/patches/pypdf-6.21.1/0001-tests-import-ssl-where-it-is-used.patch,recipes/patches/cloudpickle-3.1.2/0001-tests-import-psutil-where-it-is-used.patch,recipes/patches/formulaic-1.2.2/0001-tests-skip-the-pyarrow-check-without-pyarrow.patch,recipes/patches/jinja2-3.1.6/0001-tests-run-the-asyncio-cases-without-trio.patch. sympy's doctests run under sympy's own doctest runner, output checker and printer setup (the setup once per session, with the stdlib's finder: module globals, anddoctest_depends_onnot read); six sympy tests that do not end within 2.9 to 4.5 T of fuel, sympy'stest_W25, and networkx'stest_asadpour_integral_held_karp, whose path is not the same on Linux, are traps, named with their reason. -
Two tiers of tests (spec 102). The slow tests run before a publication, never left out: numpy's and pandas'
slowcases, contourpy's slow andrandom_bigcases and CPython'scpu-gated tests form a release tier, judged on the tag, on the dry run on main and in the lane's release acceptance (--tier release); every other run judges the normal tier, about 4 T lighter. A row a run's tier did not run keeps what was accepted, a check that moves between tiers needs--accept-tier-move, andcontext/release-tier-claims.tomlholds the claims between two releases. -
The release runs scipy's, scikit-learn's and statsmodels' own tests (spec 095). Each suite runs whole in the release tier, 261 groups in 104 Linux release shards, about 86 T of guest fuel; a smoke group per package (two for scipy) runs in the normal tier at every check, 855 G in two Linux shards, chosen so every subpackage with compiled modules and every carried patch is touched, a rule the judge holds against the product's builtin modules. Every known failure is a class by test id with its closer; every file not run is named with its reason. The judge gains three keys for these families (the ancestors'
__init__.pystaged, test data scipy's build makes from its sdist made in the guest, a family's environment) and a clock step for a release group whose one test times a call. -
One module touches wasmtime-py's private API (spec 103).
src/wasi_python/_wasmtime_ffi.pyis the engine seam: the runner and the mounts drive the engine through its named operations, it prefers wasmtime-py's public API wherever the installed release has it, and at the firstrun()it raises oneImportErrornaming the wasmtime-py version and every private name that moved. A rules check holds that no other.pyundersrc/ortests/reaches the private API; a second holds the debt sheetcontext/upstream/34-wasmtime-py-public-api-for-the-runner/facts.mdequal to the names the seam needs. No behaviour, fuel or errno changes. -
The runner is a package (spec 104; the runner design review's H1, M5, L1).
runner.py(6,241 lines) is eleven modules, one per concern (_limits,_platform,_artifacts,_engines,_private_dirs,_zip,_places,_home,_call,_sessions, andrunneras the thin entry), the text moved verbatim, in a written import order with no cycle;__init__.pyno longer importsrunnerlast; a unit check finds every module-level lock and proves the fork hook re-makes it; a rule holds every module undersrc/to 1,500 lines. Nothing the embedder sees changes: no public name, no message, no fuel count. -
The runner's process state has one owner (spec 105): every lock, cache, counter and registry of the runner is an attribute of one
_Processobject, reset for a fork by introspection over its locks and released at exit by one method; the judge's seams are fields of that object, not module globals; two rules checks keep locks and mutables out of module level. No public name changes. -
The runner's checked call is a frozen typed record, a session's and a home's life are transition tables the judge drives whole, and
mypy --strictruns oversrc/wasi_pythonin the judge (spec 106). -
The runner's mount table, platform choice, file stamps and comments explain themselves (spec 107; the design review's H4, M3, M4 and M2).
mounts.pyasks nine questions of a call-context protocol and imports nothing from the runner; one_platform.pyholds every POSIX, macOS and Windows difference behind one interface chosen once at import; the digest and version caches in front of the artifact check key on the full file stamp, so a same-size, same-time replacement is hashed again;src/names no specification, memo, review, finding ID orcontext/path, every reason kept in words, held by one rules check (the owner's rule, "code speaks for itself";tests/,scripts/andrelease/follow in spec 109); andmypy --strictis clean over the whole package. No public name, artifact byte or guest-visible errno changes. -
A structural review is a stage (spec 108; the design review of 2026-10-20). A change that adds a module-level mutable or lock, a use of wasmtime-py's private API, a platform branch or a test seam outside the one place specs 103 to 107 give each, or takes a module past the size ceiling or a function past 80 lines, is reviewed before it is built; every release gets a structural review of
src/against the design review's eight-row rubric, beside its security review, which the judge requires of a released version; and two rules checks hold that the judge patches the runner only through its process object and that no function is over 80 lines, each with an allowance list that only shrinks. Nothing in the artifact set or the wheel changes. -
Code speaks for itself outside
src/(spec 109). Comments and docstrings undertests/,scripts/,release/and the release workflow give their reasons in words, with no specification number, review, finding identifier orcontext/path; a rules check holds it, with the judge's allowance files, the guest's own sources and thesrc/check's own documentation of its shapes named as the exceptions. No code, behaviour, fuel or artifact byte changes;MANIFEST's recipe hashes ofscripts/move with their comments. -
numpy's linear algebra,
dotandmatmulrun on the set's OpenBLAS (spec 100; questions 106 and 107). numpy was built with no BLAS and ran its bundledlapack_lite, an f2c LAPACK 3.2.2. It now finds the OpenBLAS 0.3.34 scipy uses, through numpy's own build options; a one-line recipe header (recipes/numpy_lapack_abi.h) and a file of shims (recipes/numpy_lapack_shims.c) bridge numpy'sintdeclarations to OpenBLAS'svoidsubroutines, andlapack_lite's f2c code leaves the binary. A matrix ofnanraisesLinAlgErrorinsvd,lstsqandeighas natively, where it raisedValueError: On entry to DLASCL …(andlstsq, with spec 099 alone,SystemError); numpy and scipy agree on which matrices are singular; a 200×200 inverse, decomposition or product takes 23 to 55 percent less fuel, and a 3×3 product 28 percent more (OpenBLAS's call costs more than numpy's own loop at that size);python.wasmis 1,240,316 bytes smaller and the snapshot 1,237,935. A matrix singular only in exact arithmetic is a rounding question, and the README says how the guest differs there (measured on macOS arm64), and what OpenBLAS's buffer does under a tight memory cap. The oracle's numpy on macOS arm64 is now the OpenBLAS wheel. -
tools.pyis three modules behind one public facade (spec 110).wasi_python.toolskeeps every public name and nothing else of weight; the schema rules (what a tool's schema may say, how one is derived from a signature, how a value is checked) live in_tool_schema.pyand the host side of a call (the frames, the thread, the budget, the slot table) in_tool_host.py, each under the 1,500-line ceiling, importing in one written direction;tools.pyleaves the size allowance and its two functions leave the long-function allowance, split at a seam each. No public name, message, artifact byte or fuel count changes. -
The runner's filesystem reads along the WASI interface's seams (spec 111; the design review's section 5 and R3, R7).
mounts.pykeeps the public types, their two private rows and the call-context protocol; the preview1 bytes (_fs_memory.py), the call's table and its provider bridge (_fs_table.py) and the descriptor and path calls (_fs_descriptors.py,_fs_paths.py, free functions over the table, dispatched by a table equal to the shadowed signatures) are one concern each, every module under the size ceiling and every function under 80 lines; the ceiling's allowance losesmounts.pyand the filesystem's import rule holds all five modules in a chain. No public name, artifact byte or guest-visible errno changes. -
A singular matrix raises
LinAlgError(spec 099; spec 066's survey). wasm has no floating-point status flags, and numpy reports a failed LAPACK call by one, sonumpy.linalg.inv,solve,tensorinv,tensorsolveandmatrix_power(a, -1)of a singular matrix, andcholeskyof one not positive definite, returnednans. A recipe header,recipes/npy_fenv_flags.h, keeps the signals numpy raises itself in a software flag word: these raiseLinAlgErrorwhenever numpy's LAPACK (lapack_litethen; OpenBLAS since spec 100, above) finds the matrix singular; for a matrix singular only in exact arithmetic that is not always where native numpy does (LAPACKs round differently; exact parity is not a goal in wasm). The same word makes integer division by zero, integer overflow in division and float16 overflow warn, or raise undernp.errstate, as natively: a widening of the owner's question, named here. Signals only the hardware gives (float division by zero,log(0), invalid casts) stay silent; the README says so. -
A faster release run (spec 101). A dispatch on a branch other than main reuses a set the cache holds under its commit's key (
release/set_key.py: every pin, every tracked file underscripts/andrecipes/, the constructor gate with its imports, the build's environment), and the same run still rebuilds the set from the pins three times on Linux and requires the same bytes; a pushed tag and main always build. The build shard runs on a fresh arm64 Linux build on every run, tags included. The macOS judge splits its two longest shards (own-hostile,own-embedding-a)..github/workflows/set-cache.ymlfills main's cache on each push to main. The artifact set does not change. -
The release runs the compiled packages' own tests (spec 066): matplotlib's, contourpy's, Pillow's, kiwisolver's, lxml's and regex's own suites, every test of each (contourpy's random_big,
--runslowand--runtextcases included, and Pillow'schecks/andselftest.py), in 40 groups, 17,972 passes, each package a family with its own grant, stage and budget (caps 1.5 times the measured fuel, 1.517×10¹³ in all, each held to its recorded measurement by the rules check), in fourteen new Linux release shards; known failures are 23 classes by node id, each a gap with its closer.tests/upstream/ package_drive.pydrives them; numpy's and pandas' driver and verdict are unchanged.tests/upstream/packages.toml: the six aresuites; scipy's suite now closes with spec 095. -
The OpenBLAS patch script no longer fails when macOS rewrites a
.DS_Storein the unpacked tree (spec 097).scripts/make_openblas_void_patch.shleaves host litter out of itsgit addby pathspec, so git never opens a litter file, and removes a staged one withgit update-index --force-remove, which a rewrite cannot refuse; the carried patches are unchanged. The judge's guest folders live outside the home folder on macOS, in the per-user temporary folder, where macOS's folder-size cache does not write, so a long run's writable root is no longer voided by host litter; the sealed stages stay underbuild/harness, and Linux and Windows are unchanged. -
A garbage cycle holding a kiwisolver object is collected, not a trap (spec 096; spec 066's survey). kiwisolver's
Variable,Term,ExpressionandConstraintdeclaredtp_clearas returning nothing, so the collector's call ended the guest inindirect call type mismatch; carried patchrecipes/patches/kiwisolver-1.5.1/0002-tp-clear-returns-int.patchis upstream's fix (#245), until a kiwisolver release has it.import lxml.html.diffworks: its plain-Python form lackedcython.finalwithout Cython; carried patchrecipes/patches/lxml-6.1.3/0001-fake-cython-final.patchis upstream's line (#477), until lxml 7.0. The judge's callback scan reads type slots: static type objects and their method suites,PyType_Slotarrays and module definitions, 5,893 functions on the set with scipy, where spec 051's read method and getset tables. -
statsmodels 0.15.0 built in, with formulaic 1.2.2, interface-meta 2.0.1 and wrapt 2.5.1 (spec 045). Its 26 compiled modules are builtins and its Python package is in the zip, with native statsmodels' answers on a golden job (
statistical_models): regressions, discrete and generalised linear models, tests, ARIMA and the state-space models, STL, exponential smoothing, LOWESS and KDE, with formulas in patsy's or formulaic's syntax. formulaic, its second formula engine, comes with interface-meta and wrapt, all pure wheels (wrapt runs its Python classes). One carried patch reads the datasets as package resources when they are not files, so they load from the zip:recipes/patches/statsmodels-0.15.0/0001-datasets-from-a-zip.patch(its facts sheet for the owner,context/upstream/patches/statsmodels-0.15.0/). Sixteen public-domain datasets ship; the data files of thirteen whose stated terms are not approved stay out, and their loaders raiseFileNotFoundError. The state-space smoothers' and_tools' init names are renamed per file (recipes/statsmodels-renames/). What the sandbox lacks raises: X-13ARIMA-SEATS (X13NotFoundError),get_rdatasetandwebuse(no network),hdrboxplot(no processes).LICENSES/statsmodels.txtcarries its licence, the bundled code's notices and each dataset's terms. -
scikit-learn 1.9.1 built in (spec 044). Its 69 compiled modules are builtins and its Python package and six bundled datasets are in the zip, with native scikit-learn's answers on a golden job (
machine_learning). It runs serially with the same results: no OpenMP, and everyn_jobsin the calling thread. One carried patch reads its HTML representation's style sheets as package resources, soimport sklearnworks from the zip:recipes/patches/scikit_learn-1.9.1/0001-html-repr-styles-from-a-zip.patch(its facts sheet for the owner,context/upstream/patches/scikit_learn-1.9.1/). The names that would clash in one static link are renamed per file by recipe headers (recipes/sklearn-renames/), and step 60 gains two checks for every package: no weak definition two archives share unless a review allowed it (scripts/weak_duplicates.py,recipes/weak-duplicates-allowed.txt), and the link map shows scikit-learn's three random generators under their own names. Thefetch_*datasets fail without a network (with no root,FileNotFoundErroron'~'; with a root,URLError); the two sample photographs are left out, so their loaders raiseModuleNotFoundError.LICENSES/scikit-learn.txtcarries each bundled component's notice and the attribution of the three datasets UCI publishes under CC BY 4.0 (digits, breast cancer, wine). -
scipy 1.18.1, with OpenBLAS 0.3.34 as its BLAS and LAPACK, built in (spec 037). All of scipy but
scipy.odr(it needs Fortran; importing it says so):linalg,sparse(SuperLU, ARPACK, PROPACK,csgraph),optimize(HiGHS'slinprogandmilp),integrate,interpolate,signal,fft,special,stats,spatial(qhull,cKDTree),ndimage,cluster,ioand the rest, with native scipy's answers on three golden jobs. No Fortran compiler is used: OpenBLAS's own C translation of LAPACK is the LAPACK. Carried patches:recipes/patches/OpenBLAS-0.3.34/0001-c-lapack-subroutines-return-void-1-of-2.patchandrecipes/patches/OpenBLAS-0.3.34/0002-c-lapack-subroutines-return-void-2-of-2.patch(OpenBLASdevelop's convention, every C LAPACK subroutinevoid, made by our own script; merged upstream after 0.3.34),recipes/patches/OpenBLAS-0.3.34/0003-ilaver-returns-void.patch, andrecipes/patches/scipy-1.18.1/0001-uarray-callback-signatures.patch(five uarray callbacks) andrecipes/patches/scipy-1.18.1/0002-sqrtm-triu-imports-its-error-where-it-raises.patch(soscipy.linalg._matfuncs_sqrtm_triuimports first, which pydoc'saproposandhelp('modules word')do with every builtin).scipy.spatiallinks the one qhull matplotlib's_qhulllinks (step 37's 8.0.2, now with aqhull_r.pc), and scipy's own fix to its vendored copy is carried for both:recipes/patches/qhull-8.0.2/0001-format-strings.patch(upstream's merged pull request 143, unreleased). The reactor refusesfreopen(qhull'sTI <file>option, its one caller, fails as an unopenable file does), so thefd_renumberimport matplotlib's_qhullbrought goes again. OpenBLAS's kernels use wasm SIMD: hosts needsimd128(every supported wasmtime has it). Known gaps until spec 038:mmreadandmmwriteraise (a thread pool);qmc.Sobolwith nothing writable gives wrong numbers with exit 0 and an "Exception ignored" message (unscrambled all zeros, scrambled one point repeated;differential_evolution(init='sobol')andshgo(sampling_method='sobol')with it; give a temporary folder);geninvgauss.cdf,genhyperbolic.cdf,studentized_rangeandloadarffraiseOSErrorfrom thectypesstand-in.fft'sworkers=runs serially;differential_evolutionandbrutewithworkersother than 1, andcKDTree.query(workers=...)other than 1, raise. numpy's own LAPACK (lapack_lite) now carries a symbol prefix so it cannot meet OpenBLAS's; numpy computes as before. (Later in this release numpy links the same OpenBLAS andlapack_liteleaves the binary: spec 100, above.) The set is the same bytes from every build host with no exception: wasi-sdk 34 (spec 086) compiles scipy's ducc0 FFT to one object, so the same-bytes exception this specification built on wasi-sdk 33 was withdrawn before it landed. -
scipy's sandbox edges work (spec 038; spec 037's three known gaps closed). With nothing writable and no thread:
scipy.io.mmreadandmmwriteread and write serially, since Matrix Market's default parallelism is 1 on WASI (recipes/patches/scipy-1.18.1/0003-io-matrix-market-serial-without-threads.patch);stats.qmc.Sobol,qmc_quad,sobol_indicesand the Sobol options ofdifferential_evolutionandshgoread the direction numbers' bytes from the zip without a temporary file, so they no longer give wrong numbers with exit 0 (recipes/patches/scipy-1.18.1/0004-stats-sobol-without-a-temporary-file.patch); andgeninvgauss.cdf,genhyperbolic.cdf,studentized_range,tukey_hsdandloadarffwork. Changed: thectypesstand-in holds values: an instance of a numeric type (c_int(5).value,c_ulong(-1).value == 4294967295,c_floatrounded to single precision) or ofc_void_p(a 32-bit integer, or None), andcast(x, c_void_p)of an integer, None or ac_void_p, where each raisedOSErrorbefore; numpy'sndarray.ctypes.dataanddata_as(ctypes.c_void_p)give the array's address. An instance is false when its value is zero and unhashable, as in CPython;c_void_ptakes only anint; its bytes cannot be read (bytes()andmemoryview()of it raiseOSError). Nothing has an address:byref,pointer,addressof, typed pointers, strings, structures and every library load still raiseOSError, and-Vstill says "no ctypes". -
Added: seaborn 0.13.2 and reportlab 5.0.1 (spec 042; the owner's decision 43), pure Python, with native Python's output on two golden jobs: seaborn's relational, distribution, categorical, grid, matrix and
seaborn.objectsplots, its regression, kernel densities, violins and clustering (through scipy, whose importimport seabornnow pays); reportlab's canvas and platypus PDFs, charts throughrenderPDFandrenderSVG, Unicode text and images. seaborn comes from its sdist, whoselicences/folder holds the notices of the five pieces of code it vendors;LICENSES/seaborn.txtcarries them. reportlab is its wheel, without its fonts (their licences are not approved, and reportlab cannot read its own files from a zip on any Python from 3.13; for Unicode text register DejaVu Sans frommatplotlib.get_data_path()) and, until a later change ships it, without its barcode folder.LICENSES/reportlab.txtcarries the third-party notices its code holds only as comments: Tyler C. Sarna's (the barcode modules), Adobe's (the glyph list) and the QR encoder's with the MIT text. reportlab writes PDFs in memory with nothing writable (invariant=1for repeatable bytes);renderPMraisesRenderPMError(no rlPyCairo). seaborn needs a writableMPLCONFIGDIR, as matplotlib does;sns.load_datasetcannot fetch (through the runner with a writable root, its cache folder goes under the runner's freshHOMEin/tmp, removed after the call; an embedder that gives a writable root and noHOMEgets a folder/~/.cache/seabornin that root): read a granted CSV withpd.read_csvinstead. -
The judge: spec 084's read-only removal rows are blocked on Windows by a named need (spec 094). A Windows guest deletes a read-only file itself (084's C2, settled by wave 01's dry run), so the rows that check a removal keeps such a file now name the need
read-only files, unmet on Windows alone, instead of skipping without a need; the release's gather allows that block on Windows only. Nothing changes for the product. -
A desktop's files never change the build (spec 090). The build skips one shared list of names an operating system writes into folders (
.DS_Store, AppleDouble._xfiles,Icon\r,Thumbs.db,desktop.ini,ehthumbs.dband a volume's own folders such as.TemporaryItems), where it skipped three: in the stdlib zip's walk, the licences and the artifact set's list. The release tarball packs exactly the files the set'sSHA256SUMSlists, never the folder. The judge's set and wheel checks forgive those names and no others, and the wheel may carry nothing the list does not name. Step 20 removes the bytecode CPython's build writes intoLib/(its build-Python runs with-E, and its WASI interpreter under wasmtime sees no host variable), and the test suites'left_outfact no longer records bytecode or litter, so two trees built from the same pins record the same facts. The artifacts' bytes do not change. -
Each guest runs on a stack of its own (spec 078; question 102). The runner runs every guest on a thread it makes for the call, with 10 MiB of stack, 8 MiB of it for wasm (it was 512 KiB on the caller's thread), and checks that thread's stack before any wasm runs. Recursion in C over nested data (
repr,==,jsonboth ways), the parser, deep exception groups, expat's callbacks, XPath and XSLT now raise instead of ending the call in a trap; freeing nested structures, deephash, long expressions, C14N, RelaxNG and very high recursion limits still trap, 10 to 16 times deeper (the README lists them). A recursing guest can no longer kill a host whose calling thread is small, such as musl's 128 KiB threads andrun_async's pool there. Yourhostfunction and providers still run on the thread that calledrun. Ctrl-C now stops a guest that computes without calling back, and an embedder's SIGINT handler that raises stops the call instead of being lost. Spec 065's nine trap entries close: their tests run in their groups,test_tracebackwhole. Changed: each running guest holds one more thread (10 MiB of stack reserved; committed on Windows); a door or provider call costs one thread hand-off each way; every call's epoch deadline is checked every 50 ms. On CPython 3.14.0 on musl every call is refused (use 3.14.1 or later), and so is every call on a platform whose thread stack the runner cannot read (FreeBSD; any but Linux, Android, macOS and Windows). No public name, signature or default changes; fuel does not change. -
The release's gather checks what it trusts (spec 085; the 0.1.0a7 security review's F5, F6, F7 and N5). A shard's record may report a check blocked only by a need that check's own code asks for, and only on a system that lacks it (F6). Its unit-test tally must equal its share, as whole numbers, and every passing check must carry each metric the lane accepted for it, as a number; every record must name the wasmtime
uv.locknames (F7). Each acceptance's history line now records the hash of the artifact set's ownSHA256SUMS, the build job hands the same hash on, and the gather requires the two to agree, so the lane's acceptance covers the snapshot, the zip,MANIFESTand the licences as well aspython.wasm(F5). The rebuild jobs print the runner's ownpython3 --version(N5). The gather checks consistency, not authenticity: a compromised judge job could still write consistent records, though never change the bytes that are signed or published. -
Fixed: a guest could make its private root outlive the call (spec 084; the 0.1.0a7 security review's F1). A guest given
root=Truethat built a tree one level at a time throughdir_fd, deeper than the process's free descriptors (247 levels at macOS's default soft limit of 256), left its private root and lock file behind, silently, and the next process's sweep failed the same way, so disk use added up across calls; a deep freshHOMEwas left in the caller's root the same way. The runner now removes its folders with a walk that holds two descriptors at any depth, creates nothing, never follows a link and changes no mode, so content the host made read-only is left, never deleted, even when the guest moves it in. A folder a removal leaves is named in oneRuntimeWarningper process; a private folder keeps its lock file and is retried by the next call's sweep (three times per process, then by the next process). AHOMEin a caller's root, which another call on that root may be writing, gets a budget of about a million system calls. No public name, default or field changes. -
Fixed: on CPython 3.11 a guest could make
runraiseRecursionErrorand keep its private root's lock for the process's life (spec 084). 3.11'sshutil.rmtreerecurses one frame per level, so a private root of more than about 995 levels (1,010 at Linux's common soft limit of 1,024) made the removal raise out ofrun. The new removal does not recurse, and a removal that fails in any way now leaves the folder with its lock free for the sweep. -
The abandoned-turn check holds its guest instead of racing it (spec 093). Spec 078's check that a turn forced out of its home keeps its session busy read
busywhile the stopped guest was ending, and on Windows the guest ended first. It now holds the guest thread on a gate until it has looked, and checks that the teardown runs on the guest thread. The runner does not change. -
The judge compares a release run with a local one (spec 092). The release's merged record now carries what the comparison needs (the checks no shard ran, the files collected, each shard's unit tally, anything that steered a shard, and which system's run each check's row comes from), and
tests/harness/sameness.pyreads it as the darwin run it contains. With--claims, it prints only the differences no specification's claim list explains. Judge only: no product change. -
Changed: the toolchain is wasi-sdk 34 (clang 23.1.0, wasi-libc
2e6fb9d8with musl 1.2.6, libc++ 23.1.0; spec 086). wasi-sdk 33's clang nestedtry_tableblocks by heap address in one pass of its WebAssembly backend, so some C++ (scipy's FFT, coming) compiled to different bytes on every run; LLVM 23 fixed it. Every artifact's bytes change once, and every build host still gives the sameSHA256SUMS, with no exception. The judge compiles the reduced case 64 times with each pinned clang and expects one object, and records every#defineof the guest'spyconfig.h. Programs now seeos.dup2,os.pipe2andtime.pthread_getcpuclockid, each raisingOSErrorENOTSUP, andselect.PIPE_BUF(4096): wasi-libc 34 declares them as stubs.os.dupkeepsENOTSUP: wasi-libc 34'sfcntlanswers its two dup commandsEINVALon WASI preview1 while itsdupanswersENOTSUP, so a new link-time refusal,recipes/fcntl_dup_refusal.c, givesdup's answer (a facts sheet for wasi-libc,context/upstream/25-wasi-libc-fcntl-dupfd/). GB18030 stays refused: musl 1.2.6's decoder still has the bug. -
Changed: strings hash with SipHash-1-3 (spec 086; the 0.1.0 security review's S5). CPython's configure falls back to 32-bit FNV when a cross build cannot run its aligned-access probe, and the guest hashed
str,bytesandmemoryviewwith FNV, which crafted keys can flood whatever its key (PEP 456). Step 20 now passes--with-hash-algorithm=siphash13, CPython's own default:sys.hash_info.algorithmis'siphash13'on both binaries, kept in 32 bits on wasm32. The snapshot's key stays fixed and public, which the README's Not protected list now says. -
Fixed: a missing stdlib module says why. The zip now carries
_missing_stdlib_info, which CPython's build generates beside_sysconfigdata, soimport _tkinterfails with "Unsupported platform for standard library module '_tkinter'" as natively, not "was not found" (spec 086; spec 078's build review, F2). -
The judge's inline pytest sessions keep wasmtime's signal handlers (spec 082; spec 080's review, F1). A pytest session the judge runs inside its own process ended by switching faulthandler off and on again, which dropped wasmtime's handlers on Linux, so the runner's guard refused 16 later unit tests in a serial run. The judge's
pytesternow runs every such session with-p no:faulthandler, and a check holds it. The runner does not change. -
CPython's own tests in the judge and the release (spec 065). The judge runs CPython's own regrtest suite in the guest: 542 of its 557 modules in five groups, 28,109 passing tests, each known failure a class by test id with its closer, each test it does not run named, each result that differs by host recorded per system (26, most of them Windows file rules), and the eight tests where freeing or parsing deep structures still traps the guest (spec 078) each a gap. The judge states each guest clock's resolution instead of passing the host's through. The release runs the five groups on Linux in its existing upstream shards, and the file-system group on macOS and Windows too.
pythonnow names its suite intests/upstream/packages.toml. The hostile-guest checks that read MANIFEST are blocked, not failed, without an artifact set. -
The judge answers a file's times and inode number itself (spec 083). Its guest driver forwards the guest's file-stat and listing calls to wasmtime and answers the times from its own stepped clock and the inode numbers from a count, so guest fuel no longer moves with the host's file times or inode numbers (it did, in steps of 17 instructions), and a file the guest writes is no longer dated after the guest's clock. Each run gets its own copy of the stdlib zip instead of a hard link to the shared one, so no run sees another's link count. CPython's two
test_utime_currenttests now pass in the judge. The product runner is unchanged: programs run throughwasi_python.runstill see the host's real times and inode numbers. -
Fixed: regex's
timeout=fires past 2.1 seconds (spec 087; the 0.1.0a7 security review's F3): a second carried patch,recipes/patches/regex-2026.9.29/0002-timeout-ticks.patch, keeps regex's clock ticks in 64 bits (wasm32'sPy_ssize_tis 32 bits, and wasi-libc counts 10**9 ticks a second);timeout=infnow means no timeout. Checks now hold the README's lxmlDENY_ALLsentences and each 512 KiB recursion row, on an engine at wasmtime's defaults. -
Fixed: an installed copy never loads a neighbouring artifact set (spec 087, Decision 10; the 0.1.0 security review's S3). Only a source checkout of wasi-python (
src/wasi_python/beside apyproject.tomlnaming it) prefers itsbuild/artifacts/; a copy installed withpip install --target, or vendored, inside a project uses its own set.runandSessioncheck the set they find against itsSHA256SUMSbefore using it, compile and grant only the bytes they checked, and raiseArtifactsMissingon a difference; a set passed asartifacts=is the caller's own, as before. No public name, signature or default changes. -
Added: tier A of the default profile, thirty pure-Python packages (spec 039; the owner's decisions 43 to 47). By family: Excel (openpyxl 3.1.5, et-xmlfile 2.0.0, xlsxwriter 3.2.9, xlrd 2.0.2); PDF (pypdf 6.21.1); HTML and XML (beautifulsoup4 4.15.0, soupsieve 3.0.3, html5lib 1.1, webencodings 0.6.1, defusedxml 0.7.1, xmltodict 1.0.4); text formats (pyyaml 6.0.3, tomli-w 1.2.0, jinja2 3.1.6, markupsafe 3.0.4, tabulate 0.10.0, charset-normalizer 3.5.2); modelling (networkx 3.7, patsy 1.0.3); symbolic (sympy 1.14.0, mpmath 1.3.0); utilities (attrs 26.1.0, more-itertools 11.2.1, toolz 1.2.0, tqdm 4.70.1, typing-extensions 4.16.0, joblib 1.6.0, cloudpickle 3.1.2, threadpoolctl 3.7.0, narwhals 2.27.1). Each is pinned by URL and SHA-256; 28 are pure wheels (charset-normalizer's pure wheel, not its mypyc ones), and pyyaml and markupsafe, which publish no pure wheel, come from their sdists' pure package folders, so pyyaml runs without libyaml (
yaml.__with_libyaml__isFalse, noCLoader) and markupsafe through_native. Each family's golden job equals native Python's output. The zip grows by 2,014 members and 16.5 MB (38.7 MB); the plain binary is unchanged; a plain-c passcosts 50.6M more guest instructions (218M) and a snapshot call 4.7M more (20.4M). No package is pre-imported. Their licence notices are inLICENSES/; tqdm (MPL-2.0 AND MIT) ships with the full MPL-2.0 text and its source, the pinned wheel unchanged, asLICENSES/tqdm-4.70.1-source.whl, and sympy's notice carries the MIT notice of its LaTeX grammar. -
Changed: the
ctypesstand-in'sctypes.util.dllist()answers[](spec 039): the guest is one statically linked module and loads no shared library, so threadpoolctl finds no native thread pools (threadpool_info()is[]) instead of raisingOSError. -
The default profile's edges, documented (spec 039; the README's What the default profile carries): with nothing writable,
.xlsxis written only through xlsxwriter in memory (engine_kwargs={"options": {"in_memory": True}}); a writable/tmpgrant makes every writer work (decision 46). pypdf's AES needscryptography, which is not carried (DependencyError). joblib runs serially. tqdm warns once that it cannot start its monitor thread (tqdm.tqdm.monitor_interval = 0silences it). Two known gaps until spec 041: pandas' Styler (df.style,df.to_latex()) raisesTemplateNotFound, and networkx's graph atlas raisesTypeError, each reading its files by path from the zip. With jinja2 present,.styleraisesTemplateNotFound(aLookupError) instead ofAttributeError, sohasattr(df, "style"),getattr(df, "style", None)andinspect.getmembers(df)now raise until spec 041.yaml.unsafe_load,sympy.sympifyon a string, plain jinja2 templates and pickle run code from their input inside the guest, as natively; the safe forms refuse.pd.read_htmlof a GB18030 page whose tables lxml cannot read now falls back to bs4 and raisesUnicodeDecodeError(ImportErrorbefore). -
Fixed: pandas' Styler and networkx's graph atlas read their files from the zip (spec 041).
df.style(to HTML, LaTeX, text and Typst),df.to_latex(),Styler.from_custom_templateandnx.graph_atlas,nx.graph_atlas_gwork, with native Python's output, where they raisedTemplateNotFoundandTypeError;hasattr(df, "style"),getattr(df, "style", None)andinspect.getmembers(df)work again. Two carried patches, each an upstream bug in any Python that imports the package from a zip, with a facts sheet drafted for the owner undercontext/upstream/patches/:recipes/patches/pandas-3.0.6/0001-styler-templates-from-a-zip.patch(jinja2'sPackageLoaderwhen the templates folder is not a directory) andrecipes/patches/networkx-3.7/0001-graph-atlas-from-a-zip.patch(open the atlas resource, then decompress the stream). -
Changed: networkx is built from its sdist (spec 041), pinned in place of its wheel, so its carried patch has a source tree: its package folder,
METADATAandentry_points.txtare the wheel's byte for byte, but fornetworkx/conftest.py, pytest's configuration, which the zip no longer holds.make_stdlib_zip.py --entry-pointspacks an sdist'sentry_points.txtbeside itsMETADATA. -
Fixed: a file a program leaves open keeps its buffered bytes, in every mode (spec 073). On the snapshot,
runlost them when__main__defined a function, or when the program hung the file on a pre-imported stdlib module or class (json.JSONEncoder.default = …); a script, a directory and-mlost them on both binaries. Now every program runs in a fresh__main__, a script and-mrun in it as CPython's own entry points run them, andrunon the snapshot undoes the snapshot's frozen heap before it finalizes, as a session's close does (the owner's answer to question 103), so the snapshot finalizes as CPython does: aZipFileleft open gets its directory written too. The price is the finalization: a bare snapshot call costs about 43.8M guest instructions instead of 15.6M, most of it after the program has ended. So afuel=cap on a snapshot call must cover about 28M more (more after large imports, up to about 90M), or the call ends"out of fuel", withexit_codeNone, after its output was written. -
Changed: a script's, a directory's and
-m's__main__are CPython's own (spec 073). A script's__package__isNoneand its__loader__aSourceFileLoader(aSourcelessFileLoaderfor a.pyc), and its__file__is removed when it ends, as CPython does;__builtins__is the builtins module in every mode; anatexithandler finds the program's__main__, its classes and functions;-mof a missing module prints…/python.wasm: No module named …, a directory without__main__.pyprintscan't find '__main__' module in …, and an-mtraceback shows runpy's_run_module_as_mainand_run_codeframes, each as CPython does. No public name, signature or default changes. -
matplotlib's second milestone: triangulations, contours, SVG and PDF (spec 043).
matplotlib.tri.Triangulation(x, y)(Delaunay, through matplotlib's_qhulland qhull 8.0.2), and sotricontour,tricontourf,tripcolorandtriploton bare points, now work, and so docontour,contourfandclabel, through contourpy 1.4.0, built in ascontourpy._contourpywith its package in the zip. A golden job saves a figure with all of them as SVG and PDF: the SVG's bytes are native matplotlib's, and the PDF's are too but for its font subset tags, which matplotlib derives fromhash(), 32 bits wide in the guest. One more carried patch,recipes/patches/matplotlib-3.11.2/0002-qhull-without-devnull-on-wasi.patch: on WASI,_qhullsends qhull's messages to a stream that writes nowhere instead of opening/dev/null, which raisedRuntimeError: Could not open devnullin every Delaunay triangulation and, in a writable root holding adevfolder, would have made a file there; its facts sheet is for the owner to send (matplotlib refuses AI-written pull requests). contourpy runs one thread (max_threads()is 1). The binary imports one more WASI preview1 call,fd_renumber(qhull'sfreopen), which the runner already shadows and counts.python.wasmgrows by 1.08 MB and the zip by 19 members;import matplotlib.pyplotcosts what it did. The notices gain contourpy's licence with matplotlib'sLICENSE_YORICK(GIST's notice, for contourpy'smpl2005code; question 81) and qhull'sCOPYING.txt. -
HOME is never the root (spec 114; the 0.1.0 security review's H2, L1, L2 and L3). Where the runner makes no fresh HOME (no root, a read-only grant or a mount at
/tmp, a mount at/), the guest'sHOMEis now an empty, read-only folder of the call's own, so~never falls back to the working folder, a writable root. Before, a reused root let one call plant~/.reportlab_modsor~/.reportlab_settingsand a later call's bareimport reportlabran it. The stdlib zip also carries emptyreportlab_modsandreportlab_settingsmodules, which reportlab imports before it would read either file, so it reads neither, whatever HOME the embedder passes. Areportlab_modsof the embedder's own onimport_pathis now shadowed; useRL_*variables. The README names what anenv=HOME in a reused root still carries (pdb's.pdbrc, scikit-learn's pickled caches), matplotlib's./matplotlibrcand a planted/etc/xml/catalog. -
Every module cache is guarded, whatever a call's own
cache_dir(spec 113; the 0.1.0 security review's H1). A call withcache_dir=Falseor a cache of its own no longer passes a writable grant orrootover the cache other calls and processes use: the judge now refuses one at, inside or above the call's own cache,$WASI_PYTHON_CACHEand every platform default a process of this user could compute (with or without$XDG_CACHE_HOMEor%LOCALAPPDATA%, under$HOMEand the system's home), made or not, and through every folder on the cache's path as written, inrun,run_asyncandSession. Before, a guest given such a grant could write a compiled module under the snapshot's public name that the next call loaded as native code. On POSIX a cache folder is used only at0700and a.cwasmloaded only at0600with one name, as the runner makes them; a file that is not is compiled again and replaced, never loaded. That stops a file a guest makes, not one it rewrites in place: the refusal is the guard. On Windows the cache refuses links and second names; it cannot tell who wrote a file, and the README says so. No public name, artifact byte or guest-visible errno changes. -
Strings speak for themselves (spec 112). The judge's messages, help texts and titles, the
goalmarker, the build's refusal, the release workflow's step names and the comment in the generated inittab source give their reasons in words, with no specification number, review or constitution section; a rules check over string literals holds it, with a rule's own name, a scanner's fixture, a gap's closer and one fuel-recording guest program named as the exceptions. No behaviour, fuel or binary byte changes;MANIFEST's recipe hashes of twoscripts/files move.
Verify a download (gh logged in; see the README's Verifying a download):
gh attestation verify <asset> --repo ClayGendron/wasi_python \
--signer-workflow ClayGendron/wasi_python/.github/workflows/release.yml@refs/tags/v0.1.0 \
--source-ref refs/tags/v0.1.0 --deny-self-hosted-runners