PyScript

How to run PyScript in React

Trying to run PyScript in React can be a bit tricky.

Table of Contents
  1. Setting up Pyscript
  2. Digging into the problem
  3. Looking into whitespace being stripped
  4. Trying to fix the issue
  5. The Fix

A user asked a question in the pyscript community discord saying that when trying to use pyscript in a NextJS app, the following would occur:

python
1PythonError: Traceback (most recent call last): File "/lib/python3.10/asyncio/futures.py", line 201, in result raise self._exception File "/lib/python3.10/asyncio/tasks.py", line 232, in step result = coro.send(None) File "/lib/python3.10/site-packages/_pyodide/_base.py", line 506, in eval_code_async await CodeRunner( File "/lib/python3.10/site-packages/_pyodide/_base.py", line 241, in init__ self.ast = next(self._gen) File "/lib/python3.10/site-packages/_pyodide/_base.py", line 142, in _parse_and_compile_gen mod = compile(source, filename, mode, flags | ast.PyCF_ONLY_AST) File "", line 1 for i in range(9): print(i) def func(): print('function works') ^^^ SyntaxError: invalid syntax

From the exception, the code was all in the same line; this was odd. After asking for the code, the user said that the code looked like this:

python
1<py-script>
2for i in range(9):
3 print(i)
4
5def func():
6 print('function works')
7</py-script>

Something was stripping all the white spaces. If you know Python, indentation is part of the language, so if we strip whitespaces Python will throw a SyntaxError - this was exactly what was happening

Setting up Pyscript

Before we dig into how to solve the above problem, let's first see how to install PyScript. If you go to pyscript.net you can get the needed scripts to install pyscript. The user was using NextJS, and NextJS allows you to create a _document file so you can create a custom document. See the docs. All you need to do is add this file to your pages folder.

tsx
1import { Html, Head, Main, NextScript } from 'next/document'
2import Script from "next/script"
3
4export default function Document() {
5 return (
6 <Html>
7 <Head>
8 <link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
9 <Script defer src="https://pyscript.net/latest/pyscript.js" strategy='beforeInteractive'/>
10 </Head>
11 <body>
12 <Main />
13 <NextScript />
14 </body>
15 </Html>
16 )
17}

Then we can add the py-script tag in the index.tsx file to start playing with pyscript.

tsx
1export default function Home() {
2 return (
3 <div>
4 <py-script>
5 for i in range(9):
6 print(i)
7
8 def func():
9 print('function works')
10 </py-script>
11 </div>
12 )
13}

Digging into the problem

Now that we have pyscript installed, we can start looking into why the whitespace was being stripped. Looking at the DevTools console, under the sources tab, the index.js was compiled like this:

js
1(/*!*************************!*\
2 !*** ./pages/index.tsx ***!
3 \*************************/
4 /***/
5 function(module, __webpack_exports__, __webpack_require__) {
6
7 "use strict";
8 eval(__webpack_require__.ts("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": function() { return /* binding */ Home; }\n/* harmony export */ });\n/* harmony import */ var react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! react/jsx-dev-runtime */ \"./node_modules/react/jsx-dev-runtime.js\");\n/* harmony import */ var react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_0__);\n\nfunction Home() {\n return /*#__PURE__*/ (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_0__.jsxDEV)(\"div\", {\n children: /*#__PURE__*/ (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_0__.jsxDEV)(\"py-script\", {\n children: \"for i in range(9): print(i) def func(): print('function works')\"\n }, void 0, false, {\n fileName: \"/Users/fabiorosado/test/test/pages/index.tsx\",\n lineNumber: 4,\n columnNumber: 3\n }, this)\n }, void 0, false, {\n fileName: \"/Users/fabiorosado/test/test/pages/index.tsx\",\n lineNumber: 3,\n columnNumber: 2\n }, this);\n}\n_c = Home;\nvar _c;\n$RefreshReg$(_c, \"Home\");\n\n\n;\n // Wrapped in an IIFE to avoid polluting the global scope\n ;\n (function () {\n var _a, _b;\n // Legacy CSS implementations will `eval` browser code in a Node.js context\n // to extract CSS. For backwards compatibility, we need to check we're in a\n // browser context before continuing.\n if (typeof self !== 'undefined' &&\n // AMP / No-JS mode does not inject these helpers:\n '$RefreshHelpers$' in self) {\n // @ts-ignore __webpack_module__ is global\n var currentExports = module.exports;\n // @ts-ignore __webpack_module__ is global\n var prevExports = (_b = (_a = module.hot.data) === null || _a === void 0 ? void 0 : _a.prevExports) !== null && _b !== void 0 ? _b : null;\n // This cannot happen in MainTemplate because the exports mismatch between\n // templating and execution.\n self.$RefreshHelpers$.registerExportsForReactRefresh(currentExports, module.id);\n // A module can be accepted automatically based on its exports, e.g. when\n // it is a Refresh Boundary.\n if (self.$RefreshHelpers$.isReactRefreshBoundary(currentExports)) {\n // Save the previous exports on update so we can compare the boundary\n // signatures.\n module.hot.dispose(function (data) {\n data.prevExports = currentExports;\n });\n // Unconditionally accept an update to this module, we'll check if it's\n // still a Refresh Boundary later.\n // @ts-ignore importMeta is replaced in the loader\n module.hot.accept();\n // This field is set when the previous version of this module was a\n // Refresh Boundary, letting us know we need to check for invalidation or\n // enqueue an update.\n if (prevExports !== null) {\n // A boundary can become ineligible if its exports are incompatible\n // with the previous exports.\n //\n // For example, if you add/remove/change exports, we'll want to\n // re-execute the importing modules, and force those components to\n // re-render. Similarly, if you convert a class component to a\n // function, we want to invalidate the boundary.\n if (self.$RefreshHelpers$.shouldInvalidateReactRefreshBoundary(prevExports, currentExports)) {\n module.hot.invalidate();\n }\n else {\n self.$RefreshHelpers$.scheduleUpdate();\n }\n }\n }\n else {\n // Since we just executed the code for the module, it's possible that the\n // new exports made it ineligible for being a boundary.\n // We only care about the case when we were _previously_ a boundary,\n // because we already accepted this update (accidental side effect).\n var isNoLongerABoundary = prevExports !== null;\n if (isNoLongerABoundary) {\n module.hot.invalidate();\n }\n }\n }\n })();\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiLi9wYWdlcy9pbmRleC50c3guanMiLCJtYXBwaW5ncyI6Ijs7Ozs7O0FBQUE7QUFBZSxTQUFTQSxPQUFPO0lBQzdCLHFCQUNELDhEQUFDQztrQkFDQSw0RUFBQ0M7c0JBQVU7Ozs7Ozs7Ozs7O0FBU2IsQ0FBQztLQVp1QkYiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9fTl9FLy4vcGFnZXMvaW5kZXgudHN4PzA3ZmYiXSwic291cmNlc0NvbnRlbnQiOlsiZXhwb3J0IGRlZmF1bHQgZnVuY3Rpb24gSG9tZSgpIHtcbiAgcmV0dXJuIChcblx0PGRpdj5cblx0XHQ8cHktc2NyaXB0PlxuXHRcdFx0Zm9yIGkgaW4gcmFuZ2UoOSk6XG5cdFx0XHQgICBwcmludChpKVxuXHRcdFx0XG5cdFx0XHRkZWYgZnVuYygpOlxuXHRcdFx0ICAgcHJpbnQoJ2Z1bmN0aW9uIHdvcmtzJylcblx0XHQ8L3B5LXNjcmlwdD5cbiAgICA8L2Rpdj5cbiAgKVxufSJdLCJuYW1lcyI6WyJIb21lIiwiZGl2IiwicHktc2NyaXB0Il0sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///./pages/index.tsx\n"));
9 })

This may be a bit hard to look into, so lets focus on the pyscript tag itself:

js
1children: \"for i in range(9): print(i) def func(): print('function works')\"\n }

As you can see, all the whitespace was stripped. Also, our Python code was all in the same line.

Looking into whitespace being stripped

Since this was the result of the compiled code, I thought this might be coming from the compilation step. So I went to the Babel repl and pasted the pyscript code, this converted the code into:

js
1"use strict";
2
3/*#__PURE__*/React.createElement("py-script", null, "for i in range(9): print(i) def func(): print('function works')");

Interesting, the code is being turned into a React.createElement. Looking more into this, I discovered that JSX would strip whitespaces, which was why our Python code was being added to the same line with indentation stripped off.

Trying to fix the issue

Since the issue was coming from JSX, I thought that perhaps we could handle white space by using {" "}, the code inside the py-script tag was pretty ugly, but it seemed to work!

tsx
1<py-script>
2for i in range(9):{"\n"}
3{" "}{" "}print(i){"\n"}
4{"\n"}
5def func():{"\n"}
6{" "}{" "}print('function works'){"\n"}
7</py-script>

This is a pretty terrible way to write Python. But progress is progress. Looking at the documentation on React without JSX, it seems we could create a component that should run our code as is without JSX transforming it. If this works, perhaps I could make a PyScript component that would skip JSX altogether.

ts
1import React from "react";
2
3export class PyScript extends React.Component {
4 render() {
5 return React.createElement("py-script", null, this.props.children);
6 }
7}

Now with this component, I tried to pass the Python code and see if it worked.

tsx
1import { PyScript } from "../components/pyscript"
2
3export default function Home() {
4 return (
5 <PyScript>
6 for i in range(9):
7 print(i)
8
9 def func():
10 print('function works')
11 </PyScript>
12 )
13}

Unfortunately, this threw the same error as before. For some reason, the code inside PyScript was still being converted. An idea popped into my head. Perhaps I could wrap the contents into a string. In theory, it should work since the component creates the py-script tag with the code we are sending.

tsx
1import { PyScript } from "../components/pyscript"
2
3export default function Home() {
4 return (
5 <PyScript>
6 {`
7 for i in range(9):
8 print(i)
9
10 def func():
11 print('function works')
12 `}
13 </PyScript>
14 )
15}

This worked with a slight problem. It was running the code twice. Unfortunately, I still need to figure out why this is happening and will need to dig deeper into it.

The Fix

So far, I have made a lot of progress on how to get pyscript to work in React. The last thing I wanted to try was to use dangerouslySetInnerHTML to see if that could fix the issue of running the code twice.

tsx
1export default function Home() {
2 return (
3 <div
4 dangerouslySetInnerHTML={{__html:
5 `
6 <py-script>
7 for i in range(9):
8 print(i)
9
10 def func():
11 print('function works')
12 </py-script>
13 `}}
14 />
15 )
16}

Great news! This worked, and ran the code a single time!

To make things a bit cleaner, I added the PyScript code to its variable and passed it directly to __html.

tsx
1export default function Home() {
2 const pyscript = `
3 <py-script>
4 for i in range(9):
5 print(i)
6
7 def func():
8 print('function works')
9 </py-script>
10 `
11
12 return (
13 <div
14 dangerouslySetInnerHTML={{__html: pyscript}}
15 />
16 )
17}

Finally, I tried to replace the div with the py-script tag to see if that would work. Great news! It does!

tsx
1export default function Home() {
2 const pyscript = `
3 for i in range(9):
4 print(i)
5
6 def func():
7 print('function works')
8 `
9
10 return (
11 <py-script
12 dangerouslySetInnerHTML={{__html: pyscript}}
13 />
14 )
15}

This is where I stopped digging since it works as expected. I'm planning to look into why the component was causing pyscript to run twice. Once I have an answer, I'll update this post.


Reference:

Webmentions

0 Like 0 Comment

You might also like these

While working on adding tests to Pyscript I came across a use case where I had to check if an example image is always generated the same.

Read More
Python

How to compare two images using NumPy

How to compare two images using NumPy

Nx makes it easy to work with monorepost, this article will show you how you can use Nx for your projects and talks about some basic commands that will help you.

Read More
Tools

Getting started with Nx

Getting started with Nx

How to create a function to filter articles by tag. On this post I am using the javascript filter method to filter all articles.

Read More
React

How to filter all MDX articles by tags

How to filter all MDX articles by tags

A quick introduction on how to use the Elasticlurn Plugin for Gatsby together with MDX to create a search bar component and allow users to search your site.

Read More
React

How to use elasticlunr plugin with MDX

How to use elasticlunr plugin with MDX