refactor(grpc-web): top-level pyodide imports, [grpc-web] extra, in-Pyodide unit tests - #2160
Conversation
…yodide unit tests The web package now imports pyodide at module scope and is importable only under Emscripten/Pyodide. The base client gains a grpc-web extra (weaviate-client[grpc-web], marker-gated to Emscripten) as the documented install path. The unit tests stay in packages/web/tests but run inside Pyodide via ci/pyodide-e2e/units.mjs (async-native, no pytest); a conftest keeps CPython pytest from collecting them, and the CPython-only testing seams (install force flags, make_httpx_sender) are removed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Caze9m6PBSfYkt77mMKj2b
There was a problem hiding this comment.
Orca Security Scan Summary
| Status | Check | Issues by priority | |
|---|---|---|---|
| Secrets | View in Orca |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## feat/grpc-web-wasm #2160 +/- ##
=======================================================
+ Coverage 0 88.77% +88.77%
=======================================================
Files 0 305 +305
Lines 0 23787 +23787
=======================================================
+ Hits 0 21116 +21116
- Misses 0 2671 +2671 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
🟢 Approval recommended
The environment-specific refactor, installation metadata, documentation, tests, and CI changes are coherent with no unresolved correctness issues found.
Pull request overview
Refactors gRPC-Web support to target Pyodide exclusively and moves its unit tests into the actual WASM runtime.
Changes:
- Uses top-level Pyodide imports and removes CPython-only escape hatches.
- Adds the
[grpc-web]extra and updates installation guidance. - Replaces pytest execution with a Pyodide-native runner integrated into CI.
File summaries
| File | Description |
|---|---|
weaviate/connect/base.py |
Updates unsupported-environment guidance. |
weaviate/__init__.py |
References the new installation extra. |
setup.cfg |
Adds the platform-gated grpc-web extra. |
packages/web/src/weaviate_client_web/__init__.py |
Simplifies imports and bootstrap. |
packages/web/src/weaviate_client_web/_channel.py |
Restricts sender customization wording to tests. |
packages/web/src/weaviate_client_web/_httpx_fetch.py |
Moves Pyfetch import to module scope. |
packages/web/src/weaviate_client_web/_sender.py |
Removes the CPython HTTPX sender. |
packages/web/src/weaviate_client_web/_shim.py |
Removes forced shim installation. |
packages/web/tests/conftest.py |
Prevents CPython pytest collection. |
packages/web/tests/harness.py |
Adds lightweight test helpers. |
packages/web/tests/runner.py |
Adds the async Pyodide test runner. |
packages/web/tests/test_framing.py |
Migrates framing tests from pytest. |
packages/web/tests/test_httpx_fetch.py |
Runs fetch tests directly in Pyodide. |
packages/web/tests/test_shim_install.py |
Tests the real Pyodide shim installation. |
packages/web/tests/test_single_import.py |
Removes CPython-emulated bootstrap tests. |
packages/web/tests/test_transport.py |
Converts transport tests to async Pyodide execution. |
packages/web/README.md |
Documents installation and testing. |
ci/pyodide-e2e/units.mjs |
Adds unit and bootstrap scenarios. |
.github/workflows/main.yaml |
Moves package testing into the Pyodide job. |
Review details
- Files reviewed: 19/19 changed files
- Comments generated: 0
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…ls_scm The companion's version now derives from the repository's git tags (setuptools_scm with its root at the repo root) instead of a hardcoded 0.0.1.dev0, so every build carries the same version as weaviate-client; the pyodide-e2e job asserts the two built wheels match. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Caze9m6PBSfYkt77mMKj2b
… time packages/web/setup.py injects weaviate-client==<version> into the companion's requirements when the wheel is built, so a mismatched pair can never resolve at install time — the two packages share private contracts (error-string markers, exception constants). Consequence: every tag must publish both packages. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Caze9m6PBSfYkt77mMKj2b
pytest executes the packages/web/tests suite inside Pyodide under Node with --experimental-wasm-jspi: async tests run through run_until_complete, which stack-switches when the runner enters via callPromising(). This replaces the hand-rolled runner/harness with standard pytest collection, fixtures and parametrize, and makes empty or partial collection fail the run (pytest exit codes reach JS as a return value, never as an exception across the bridge). The base client's import-hook branches (missing companion, broken companion, grpc-present fall-through) are covered by subprocess tests in test/test_wasm_compat.py on CPython; the bootstrap scenario in units.mjs keeps the one path that needs real Pyodide, micropip and the wheels. Also strengthens assertions the port had weakened (identity restore on uninstall, the literal grpc version pin, install() returning True) and fixes two stale README claims about direct installs and off-Emscripten imports. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Caze9m6PBSfYkt77mMKj2b
There was a problem hiding this comment.
🟡 Changes recommended
The test dependency pins conflict, and the release job does not publish the newly required companion package.
Get a fresh assessment by requesting another Copilot review.
Review details
- Files reviewed: 20/20 changed files
- Comments generated: 2
- Review effort level: Balanced
The release job builds the weaviate-client-web wheel into dist and asserts both packages carry the same version on the artifacts actually uploaded — the companion's weaviate-client==<version> pin makes a base-only release leave the [grpc-web] extra unresolvable. The companion is wheel-only: its setup.py resolves the lockstep version from git tags, which an unpacked sdist would not have. The wheel artifact for the GitHub release includes it too. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Caze9m6PBSfYkt77mMKj2b
pytest-asyncio 0.25.3 declares pytest<9,>=8.2, so pin pytest 8.4.2 instead of the bundled 9.0.2 rather than depending on micropip tolerating the conflict. pytest-asyncio stays on 0.25.x: the asyncio.Runner-based 1.x fails under JSPI stack switching, while 0.25.x's run_until_complete-based execution works. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Caze9m6PBSfYkt77mMKj2b
Motivation
Review feedback on #2142 from @tsmith023: the web package kept itself importable on CPython through in-function
pyodideimports andforce=escape hatches — an import hack applied to the logical implementation. His suggestion: define the package coherently for its one environment (Emscripten/Pyodide) with top-level imports, and makeweaviate-client[grpc-web]the documented install path. This branch applies that approach on top offeat/grpc-web-wasmso the delta can be reviewed on its own; if we agree it's the right shape, it gets folded into #2142.Approach
Moving the
pyodideimports to module scope has consequences, and this PR takes all of them rather than half-committing:_sender.pyand_httpx_fetch.pyimportpyodide.http.pyfetchat the top of the file. The seams that existed only to keep CPython imports working —install(force=),install_fetch_transport(force=),make_httpx_sender— are deleted, and the package__init__now imports its submodules plainly instead of splitting them around_bootstrap().ci/pyodide-e2e/units.mjsmountspackages/web/tests/into a real Pyodide interpreter under Node and runs the bundled pytest over it — standard collection, fixtures,parametrize, assertion rewriting. Async tests execute on Pyodide's event loop through JSPI stack switching: pytest's runner is synchronous, sorun_until_completeneeds--experimental-wasm-jspion Node plus acallPromising()entrypoint. Without the flag the run fails loudly at startup — it can never produce a false green. pytest's exit code crosses to JS as a plain return value (empty collection is exit 5 and fails the step).conftest.pykeeps CPython pytest from collecting the directory. No running Weaviate is needed — everything goes through fake senders / a fakepyfetch.[grpc-web]becomes the install path, with lockstep enforced by the resolver. The extra carries asys_platform == "emscripten"marker: under Pyodide it pulls the companion, on CPython it is a no-op — one requirements list works everywhere. Both packages derive their version from the same git tag (setuptools_scm), andpackages/web/setup.pyinjectsweaviate-client==<version>into the companion's requirements at build time, so a mismatched pair can never resolve — the two packages share private contracts (error-string markers, exception constants) that make skew unsafe.The base client's import-hook branches (missing companion → install hint naming the extra, broken companion → its own error, real-grpc-present → silent fall-through) are plain CPython logic and are pinned by subprocess tests in
test/test_wasm_compat.pyacross the 3.10–3.14 matrix; the bootstrap scenario inunits.mjskeeps the one path that needs real Pyodide, micropip and the wheels.CI: the
grpc-web-testsjob (CPython 3.10–3.14 matrix) is deleted — it tested an environment the package no longer supports — and thepyodide-e2ejob gains a units step (with the JSPI flag) ahead of the e2e run, plus an assertion that both built wheels carry the same version. The release job builds, version-asserts and publishes both packages.Key areas for review
packages/web/src/weaviate_client_web/__init__.py— submodule imports now run before_bootstrap(). Safe because_channel/_httpx_fetchtake their grpc base classes from._shimdirectly, never viasys.modules["grpc"], but worth a careful look.packages/web/setup.py— the build-time==pin. Consequence: every tag must publish both packages (a base-only release would leave the extra unresolvable once a pinned companion is on PyPI). The release job now does exactly that: it builds both distributions, asserts equal versions on the artifacts it actually uploads, and publishes both. The companion is wheel-only — its setup.py resolves the version from git tags, which an unpacked sdist would not have. One thing CI cannot verify:PYPI_API_TOKENmust be allowed to create the newweaviate-client-webproject on first publish (a project-scoped token cannot; use an account-scoped token or register a pending publisher on PyPI beforehand).ci/pyodide-e2e/units.mjs— the JSPI/callPromising()mechanics, and the pinnedpytest==8.4.2/pytest-asyncio==0.25.3installed via micropip. That pair is metadata-coherent (pytest-asyncio 0.25.3 declarespytest<9,>=8.2); pytest-asyncio 1.x is not an option — itsasyncio.Runner-based execution fails under JSPI stack switching, verified empirically.test/test_wasm_compat.py— the three subprocess-based import-hook tests, including the grpc-present fall-through that only CPython can exercise (no grpcio wheel exists for Emscripten).Risks and mitigations
--experimental-wasm-jspion pinned Node 22: an experimental V8 flag in one CI step we fully control (the e2e step needs no flag; production browser code never touches JSPI). A missing or broken flag fails loudly at startup, never silently. Verified stable across 5 consecutive local runs; Node and Pyodide are both version-pinned.test/test_wasm_compat.py.weaviate-client[grpc-web]and a direct companion install fail until the first release carrying the extra/marker is on PyPI — the same status quo as feat(grpc-web): Pyodide/WASM grpc-web transport for the async client #2142 (wheels built from the branch until release). The README says so explicitly now.==pin: rebuilding only one wheel on a dirty tree a day apart produces an unresolvable pair (the.dYYYYMMDDsuffix diverges). The harness always builds both together.Testing
node --experimental-wasm-jspi ci/pyodide-e2e/units.mjs distpasses locally: the bootstrap scenario plus 136 pytest cases inside real Pyodide under Node (exit codes verified for pass, fail, and empty-collection runs; 5 consecutive runs stable). The built companion wheel carriesRequires-Dist: weaviate-client==<identical version>(inspected in METADATA). On CPython:test/test_wasm_compat.pypasses (18 tests, including the three import-hook scenarios) andpytest packages/web/testscollects nothing by design. The e2e suite (run.mjs, Weaviate 1.39.0 over native/v1/grpc-web) is unchanged. ruff, flake8 and pyright pre-commit hooks pass.Breaking changes
None for released users —
weaviate-client-webhas never shipped. Within the branch:make_httpx_senderand bothforce=parameters are gone from the package's public API, and running or importing the package on CPython is no longer supported at all.🤖 Generated with Claude Code
https://claude.ai/code/session_01Caze9m6PBSfYkt77mMKj2b