Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions .context/eegprep_lean_contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,19 @@ rather than as a snippet in a string.

`eegprep-lean` targets Pyodide. Pyodide 0.29.5 bundles CPython 3.13.2 on `emscripten_4_0_9`,
and a browser cannot be told to run a different one.
So `requires-python` and every dependency floor are a **ceiling** here, not a floor:
a floor above the version Pyodide bundles cannot be satisfied, because micropip has no
WebAssembly build to fall back to.
So `requires-python`, and the floor of every **compiled** dependency, are a **ceiling** here rather
than a floor: PyPI carries no WebAssembly build, so Pyodide's bundled one is the only one a browser
can have and a floor above it cannot be satisfied.

The test is whether the package publishes a pure-Python wheel, not whether Pyodide bundles it.
A `py3-none-any` wheel means micropip fetches any version straight from PyPI and the bundled build
goes unused, so that floor costs a download and nothing more. Of the packages that matter here,
numpy, scipy, h5py and matplotlib publish none and are genuinely capped; threadpoolctl is bundled
but pure Python and is not.

This inverts the usual reading and it is invisible from the code, which is why
eegprep's `tests/test_browser_dependency_floors.py` (sccn/eegprep#399) enforces it, and why
`eegprep-lean` inherits the same test.
eegprep's `tests/test_browser_dependency_floors.py` (sccn/eegprep#399, corrected in #402) enforces
it for the compiled set, and why `eegprep-lean` inherits the same test.

### Numerical agreement with eegprep

Expand Down
44 changes: 27 additions & 17 deletions .memory/pyodide-is-a-ceiling-not-a-floor.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,40 @@ metadata:
type: project
---

Pyodide bundles its own build of every compiled scientific package, and micropip has no
WebAssembly build to fall back to. So for anything that has to run in the browser,
`requires-python` and every dependency floor act as a **ceiling**: a floor above the version a
Pyodide release ships cannot be satisfied there, no matter how reasonable the bump looked.
For anything that has to run in the browser, `requires-python` and the floors of **compiled**
dependencies act as a **ceiling**: Pyodide bundles its own WebAssembly build, PyPI has none, so a
floor above the bundled version cannot be satisfied there no matter how reasonable the bump looked.

This inverts the usual reading of a floor and is invisible from the code, which is how
`threadpoolctl>=3.6.0` reached `epic/324-pyodide-browser` against a distribution shipping 3.5.0.
**The test is whether the package publishes a pure-Python wheel, not whether Pyodide bundles it.**
A `py3-none-any` wheel means micropip can fetch any version straight from PyPI and the bundled
build is simply unused, so the floor costs a download and nothing more.

Verified 2026-09-20 against `https://cdn.jsdelivr.net/pyodide/v0.29.5/full/pyodide-lock.json`:
Verified 2026-09-20 against `https://cdn.jsdelivr.net/pyodide/v0.29.5/full/pyodide-lock.json` and
PyPI:

| | Pyodide 0.29.5 |
|---|---|
| Python | 3.13.2 (`emscripten_4_0_9`) |
| threadpoolctl | 3.5.0 |
| scipy | 1.14.1 |
| numpy | 2.2.5 |
| matplotlib | 3.8.4 |
| h5py | 3.13.0 |
| | Pyodide 0.29.5 | universal wheel on PyPI | floor is a ceiling |
|---|---|---|---|
| Python | 3.13.2 (`emscripten_4_0_9`) | n/a | yes |
| scipy | 1.14.1 | none | yes |
| numpy | 2.2.5 | none | yes |
| matplotlib | 3.8.4 | none | yes |
| h5py | 3.13.0 | none | yes |
| threadpoolctl | 3.5.0 | `py3-none-any` | **no** |

I originally wrote this entry citing `threadpoolctl>=3.6.0` on `epic/324-pyodide-browser` as a live
broken install. **That was wrong**, and it is the instructive part: the package is bundled by
Pyodide, which made it look identical to the scipy case, but it is pure Python and micropip
installs 3.6.0 from PyPI without complaint. Being in `pyodide-lock.json` says nothing about whether
a different version is reachable.

**Why it matters:** the failure is an install that cannot resolve in the browser, found by a user
rather than by CI, and the bump that caused it will look routine in review.
rather than by CI, and the bump that caused it will look routine in review. Over-applying the rule
is its own cost: it produces false alarms on pure-Python packages and a guard nobody trusts.

**How to apply:** read the versions out of that release's `pyodide-lock.json` before raising any
floor on a browser-bound package, and never from a changelog. eegprep enforces this offline in
floor on a browser-bound package, and never from a changelog. Then check PyPI for a
`py3-none-any` wheel at the version you want, and only treat the floor as a ceiling when there is
none. eegprep enforces this offline in
`tests/test_browser_dependency_floors.py` (sccn/eegprep#399); copy that shape rather than trusting
review. Note that markers are evaluated with `sys_platform == "emscripten"`, so a
`sys_platform != 'darwin'` branch is the one the browser takes. Do not trust
Expand Down
Loading