|
| 1 | +# First PyScript Example |
| 2 | + |
| 3 | +Well, that was certainly a lot of prep. |
| 4 | + |
| 5 | +Let's get into PyScript and examples. |
| 6 | +In this step we'll add the "Hello World" example along with unit/shallow/full tests. |
| 7 | +We will _not_ though go further into how this example gets listed. |
| 8 | +We also won't do any automation across examples: each example gets its own tests. |
| 9 | + |
| 10 | +Big ideas: tests run offline and faster, no quirks for threaded server, much simpler "wait" for DOM. |
| 11 | + |
| 12 | +## Re-Organize Tests |
| 13 | + |
| 14 | +In the previous step, we made an `src/psc/examples` directory with `first.html` in it. |
| 15 | +Let's remove `first.html` and instead, have a `hello_world` directory with `index.html` in it. |
| 16 | +For now, it will be the same content as `first.html`, though we need to change the CSS path to `../static/psc.css`. |
| 17 | + |
| 18 | +We also have our "first example" tests in `test_app.py`. |
| 19 | +Let's leave that test file to test the application itself, not each individual test. |
| 20 | +Thus, let's start `tests/examples/test_hello_world.py` and move `test_first_example*` into it. |
| 21 | +We'll finish with `test_hello_world` and `test_hello_world_full` in that file. |
| 22 | + |
| 23 | +With these changes, the tests pass. |
| 24 | +Let's change the example to be the actual PyScript `Hello World` HTML file. |
| 25 | + |
| 26 | +## Download PyScript/Pyodide Into Own Directories |
| 27 | + |
| 28 | +Using `curl`, I grabbed the latest `pyscript.js` plus the `.map` etc. |
| 29 | +I did NOT check these into the repo. |
| 30 | +Instead, I made a `src/pyscript` directory, which also meant making a route, which also meant making a test. |
| 31 | + |
| 32 | +This brings up an interesting question about versions. |
| 33 | +Should the Collective examples all use the same PyScript/Pyodide versions, or do we need to support variations? |
| 34 | + |
| 35 | +Next up, Pyodide. |
| 36 | +I got the `.bz2` from the releases and uncompressed/untarred into a release directory. |
| 37 | +Bit by bit, I copied over pieces until "Hello World" loaded: |
| 38 | + |
| 39 | +- The `.mjs` and `.asm*` |
| 40 | +- `packages.json` |
| 41 | +- The distutils.tar and pyodide_py.tar files |
| 42 | +- `.whl` directories for micropip, packaging, and pyparsing |
| 43 | + |
| 44 | +Later we'll provide a command-line step to automate this (and to handle it for CI.) |
| 45 | + |
| 46 | +## Hello World Example |
| 47 | + |
| 48 | +Back to `src/psc/examples/hello_world/index.html`. |
| 49 | +Before starting, we should ensure the shallow test -- `TestClient` -- in `test_hello_world.py` works. |
| 50 | + |
| 51 | +To set up PyScript, first, in `head`: |
| 52 | + |
| 53 | +```html |
| 54 | +<link rel="icon" type="image/png" href="../../favicon.png" /> |
| 55 | +<script defer src="../../static/pyscript.js"></script> |
| 56 | +``` |
| 57 | + |
| 58 | +That gets PyScript stuff. |
| 59 | + |
| 60 | +To get Pyodide from a local installation instead of the network, I added `<py-config>`: |
| 61 | + |
| 62 | +```xml |
| 63 | +<py-config> |
| 64 | +- autoclose_loader: true |
| 65 | + runtimes: |
| 66 | + - src: "../../pyodide/pyodide.js" |
| 67 | + name: pyodide-0.20 |
| 68 | + lang: python |
| 69 | +</py-config> |
| 70 | +``` |
| 71 | + |
| 72 | +This was complicated by a few factors: |
| 73 | + |
| 74 | +- The [PyScript docs page is broken](https://github.com/pyscript/pyscript/issues/528) |
| 75 | +- There are no examples in PyScript (and thus no tests) that show a working version of `<py-config>` |
| 76 | +- The default value on `autoclose_loader` appears to be `false` so if you use `<py-config>` you need to explicitly turn it off. |
| 77 | +- If you reformat your code, the YAML gets mis-formatted and it is a pain to debug |
| 78 | +- We want the path to `pyodide.js` to work if you load the `index.html` directly or through a server |
| 79 | + |
| 80 | +At this point, the page loaded correctly in a browser, going to `http://127.0.0.1:3000/examples/hello_world/index.html`. |
| 81 | +Now, on to Playwright. |
| 82 | + |
| 83 | +## Playwright Interceptor |
| 84 | + |
| 85 | +We're going to be handling more types of files now, so we change the `Content-Type` sniffing. |
| 86 | +Instead of looking at the extension, we use Python's `mimetypes` library. |
| 87 | + |
| 88 | +For the test, we want to check that our PyScript output is written into the DOM. |
| 89 | +This doesn't happen immediately. |
| 90 | +In the PyScript repo, they sniff at console messages and do a backoff to wait for Pyodide execution. |
| 91 | + |
| 92 | +Playwright has help for this. |
| 93 | +The `page` can [wait for a selector to be satisfied](https://playwright.dev/python/docs/api/class-page#page-wait-for-selector). |
| 94 | + |
| 95 | +This is _so much nicer_. |
| 96 | +Tests run a LOT faster: |
| 97 | + |
| 98 | +- Our assets (HTML, CSS, `pyscript.js`, `pyscript.css`) are served without an HTTP server |
| 99 | +- Pyodide itself isn't loaded from CDN nor even HTTP |
| 100 | + Also, if something goes wrong, you aren't stuck with a hung thread in `SimpleHTTPServer`. |
| 101 | + Finally, as I noticed when working on vacation with terrible Internet -- everything can run offline...the examples and their tests. |
| 102 | + |
| 103 | +It was _very_ hard to get to this point, as I ran into a number of obscure bugs: |
| 104 | + |
| 105 | +- The `<py-config>` YAML bug above was a multi-hour waste |
| 106 | +- Reading files as strings failed obscurely on Pyodide's `.asm.*` files |
| 107 | +- Ditto for MIME type, which needs to be `application/wasm` (though the interwebs are confusing on this) |
| 108 | +- Any time the flake8/black/prettier stack ran on stuff in static, all hell broke loose |
| 109 | + |
| 110 | +## Debugging |
| 111 | + |
| 112 | +It was kind of miserable getting to this point. |
| 113 | +What debugging techniques did I discover? |
| 114 | + |
| 115 | +Foremost, running the Playwright test in "head-ful" mode and looking at both the Chromium console and the network tab. |
| 116 | +Playwright made it easy, including with the little controller UI app that launches and lets you step through: |
| 117 | + |
| 118 | +```bash |
| 119 | +$ PWDEBUG=1 poetry run pytest -s tests/examples/test_hello_world.py |
| 120 | +``` |
| 121 | + |
| 122 | +For this, it can help to add a `page.pause()` after the `page.goto()`. |
| 123 | +Otherwise, use the Playwright controls in the popup window to step through the execution. |
| 124 | + |
| 125 | +Next, when running like this, you can use Python `print()` statements that write to the console which launched the head-ful client. |
| 126 | +That's useful in the interceptor. |
| 127 | +You could alternatively do some console logging with Playwright's (cumbersome) syntax for talking to the page from Python. |
| 128 | +But diving into the Chromium console is a chore. |
| 129 | + |
| 130 | +When things weren't in "fail catastrophically" mode, the most productive debugging was...in the debugger. |
| 131 | +I set a breakpoint interceptor code, ran the tests, and stopped on each "file" request. |
| 132 | + |
| 133 | +Finally, the most important technique was to...slow down and work methodically with unit tests. |
| 134 | +I should have done this from the start, hardening the interceptor and its surface area with Playwright. |
| 135 | +I spent hours on things a decent test (and even `mypy`) would have told me about bytes vs. strings. |
| 136 | + |
| 137 | +## QA Tools |
| 138 | + |
| 139 | +When running `pre-commit`, it appears Prettier re-formats the YAML contents of `<py-config>`. |
| 140 | +I could have spent time to figure it out (e.g. skip those files, or teach Prettier how to handle it.) |
| 141 | +But it's not urgent, so I disabled Prettier in the `.pre-commit-config.yaml` file. |
| 142 | + |
| 143 | +Flake8 had a lot of complaints with `pyscript.py`. |
| 144 | +I edited `.flake8` to turn off all the particular problems, to avoid editing the file itself. |
| 145 | +Probably needs a better solution. |
| 146 | + |
| 147 | +`mypy` found a couple of actual bugs. |
| 148 | +Thanks, Python type hinting! |
| 149 | +(Although I did chicken out with a `type: ignore` on a bytes thing in a test.) |
| 150 | + |
| 151 | +## Could Be Better |
| 152 | + |
| 153 | +This is very much a prototype and lots could be better. |
| 154 | + |
| 155 | +There are still bunches of failure modes in the interceptor, and when it fails, things get _very mysterious_. |
| 156 | +A good half-day of hardening and test-writing -- primarily unit tests -- would largely do it. |
| 157 | +To go further, using Starlette's `FileResponse` and making an "adapter" to Playwright's `APIResponse` would help. |
| 158 | +Starlette has likely learned a lot of lessons on file reading/responding. |
| 159 | + |
| 160 | +Speaking of the response, this code does the minimum. |
| 161 | +Content length? |
| 162 | +Ha! |
| 163 | +Again, adopting more of a regular Python web framework like Starlette (or from the old days, `webob`) would be smart. |
| 164 | + |
| 165 | +We could speed up test running with [ideas from Pyodide Playwright ticket](https://github.com/pyodide/pyodide/issues/2048). |
| 166 | +It looks fun to poke around on that, but the hours lost in hell discouraged me. |
| 167 | +It's pretty fast right now, and a great improvement over the status quo. |
| 168 | +But a 3x speedup seems interesting. |
| 169 | + |
| 170 | +Finally, it's possible that async Playwright is the answer, for both general speedups and `wait_for_selector`. |
| 171 | +When I first dabbled at this though, it got horrible, quickly (integrating main loops, sprinkling async/await everywhere.) |
| 172 | +I then read something saying "don't do it unless you have to." |
0 commit comments