What's New in PyScript 2024.1.1

Published January 4, 2024

Tags: pyscript python

New Year, New Features! PyScript had it's first release in 2024 today with the release of PyScript 2024.1.1. A few banner new features have been added, including:

  • The mip package manager is now available for Micropython
  • pyscript.js_modules now acts like a module
  • The internal xterm.js terminal of scripts with a terminal attribute is exposed.

With four releases in under three months, PyScript is really hitting its release stride. As always, if you have feature requests, bug reports, or questions, please let us know on GitHub or on Discord.

MicroPython Packages

PyScript has long had a py-config tag which allows users to, among other things, install packages from PyPI or from local wheels when PyScript loads. It also smartly loads Pyodide-specific versions of some packages, with their C/Rust/Fortran extensions pre-compiled to work with Pyodide/WebAssembly.

But MicroPython, which was introduced as an additional engine for PyScript in version 2023.11.1, has an entirely different concept of package management. Rather than PyPI, most packages are installed from a resource called MicroPython-Lib using a tool called mip. Today's release brings mip to PyScript, allow users to install 3rd-party MicroPython packages in their PyScript pages.

The added packages key in <mpy-config> takes a list of strings; PyScript then calls mip.install() with each of those strings. All basic package names are looked-up on MicroPython-Lib - here's how to install, for instance, the curses.ascii package for inspecting the properties of specific characters:

1
2
3
4
5
6
7
8
9
<script type="module" src="https://pyscript.net/releases/2024.1.1/core.js"></script>
<mpy-config>
    packages = ['curses.ascii']
</mpy-config>
<script type="mpy">
    from curses import ascii
    print(f"{ascii.isalpha("A")=})")
    print(f"{ascii.isalpha("3")=})")
</script>

mip can do a lot more than just install from MicroPython-lib - it call also install files directly from a URL, from GitHub using some shorthand syntax, or a whole package specified with package.json:

1
2
3
4
<mpy-config>
    packages = ['https://raw.githubusercontent.com/micropython/micropython-lib/master/python-stdlib/bisect/bisect.py',
                'github:jeffersglass/some-project/foo.py',]
</mpy-config>

mip is now also available programmatically within Micropython via the mip package, if you need to programmatically fetch and install packages:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import mip

# Install default version from micropython-lib
mip.install('keyword')

# Install from raw URL
mip.install('https://raw.githubusercontent.com/micropython/micropython-lib/master/python-stdlib/bisect/bisect.py')

# Install from GitHub shortcut
mip.install("github:jeffersglass/some-project/foo.py")

A number of MicroPython packages are pre-installed in PyScript by default. Now, with mip integrated, the wide universe of MicroPython packages is available in the browser.

js_modules as modules

The concept of a Python Package or Module doesn't map exactly to the idea of Modules in the JavaScript world. As such, there are interesting decisions to be made when it comes to the places where these two ideas intersect.

One of these places is the js_modules key in <py-config> that was introduced in PyScript 2023.12.1. As a reminder, js_modules instructs PyScript to install a JavaScript (ECMAScript) module in the browser, and make it available to Python by a given name:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<canvas id="fw"></canvas>
<mpy-config>
    [js_modules.main]
    "https://cdn.jsdelivr.net/npm/fireworks-js@2.10.7/+esm" = "Fireworks_Module"
</mpy-config>
<script type="mpy">
    from pyscript import document
    from pyscript.js_modules import Fireworks_Module
    container = document.querySelector('#fw')
    fireworks = Fireworks_Module.Fireworks.new(container)
    fireworks.start()
</script>
[js_modules.main] "https://cdn.jsdelivr.net/npm/fireworks-js@2.10.7/+esm" = "Fireworks"

Enjoy some fireworks!

That Fireworks_Module.Fireworks.new... is a little bit awkward though. In Python, we might find it more comfortable to import the "Fireworks object" directly from the "Fireworks Module" (big ole air quotes there). Now, in PyScript 2024.1.1, you can:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<canvas id="fw"></canvas>
<mpy-config>
    [js_modules.main]
    "https://cdn.jsdelivr.net/npm/fireworks-js@2.10.7/+esm" = "Fireworks_Module"
</mpy-config>
<script type="mpy">
    from pyscript import document
    from pyscript.js_modules.Fireworks_Module import Fireworks
    container = document.querySelector('#fw')
    fireworks = Fireworks.new(container)
    fireworks.start()
</script>

In fact, any attribute of a JavaScript object can now be imported as if it was a submodule using this syntax. Hopefully, this make working with JavaScript modules feel more intuitive for Python users. See this discussion on the Pyodide GitHub for more info.

Exposed XTerm Object

When using the terminal attribute of a <script type="py"> tag, several users have reported wanting to take more direct control over the xterm.js Terminal object that's used for the output. That Terminal is now present as the terminal property of the script tag itself.

When running code in the main thread, the document.currentScript helper can be used to quickly grab the current script tag. Here's an example which resizes the terminal to 40 columns by 6 rows, then prints a long string to demonstrate the new size:

1
2
3
4
5
<script type="py" terminal>
    from pyscript import document
    document.currentScript.terminal.resize(40, 6)
    print("Lorem ipsum dolor sit amet consectetur, adipisicing elit. Repellat alias atque quae ad voluptatem officia delectus voluptatum autem possimus ut libero, neque ipsum nam non totam iusto ratione quibusdam esse.") 
</script>

Code running in workers does not update document.currentScript, since there could indeed be multiple scripts running at the same time. So a different method must be used to select the appropriate script tag. Since only a single PyScript tag may have a terminal attached (currently), the following should consistently work (the example code achieves the same effect as the one above, then opens a REPL).

1
2
3
4
5
6
7
8
9
<script type="py" terminal worker>
    from pyscript import document
    term = document.querySelector('script[terminal]').terminal
    term.resize(40, 6)
    print("Lorem ipsum dolor sit amet consectetur, adipisicing elit. Repellat alias atque quae ad voluptatem officia delectus voluptatum autem possimus ut libero, neque ipsum nam non totam iusto ratione quibusdam esse.") 

    import code
    code.interact(local=globals())
</script>

Here's a live-running example of the above code. Try running dir(term) to inspect the Terminal object.

Community Updates

PyScript Maintainer Nicholas Tollervey has been hard at work improving PyScript's visibility and community presence. To that end, the every-other-week PyScript FUN meetings - which showcase cool demos and experiments from the PyScript devs and community - have moved to Discord! Come join us every-other-thursday to see what's been cooked up with PyScript - and share something of your own!

Speaking personally, I find these calls to be a ton of fun - the demos span the gamut from deeply technical to quirky to fun and festive. If you're interested in getting to know PyScript a little better, come check them out!