Octoframes/jupyter_compare_view

Jupyterlite demo

psychemedia opened this issue ยท 33 comments

The simplest way to create a Jupyterlite demo requires access to a Python package on PyPi, or a prebuilt wheel that can be downloaded from the repo.

You can build a wheel by running pip3 wheel . in the root directory. (It's perhaps most convenient to create a Github Action to create the wheel on a repo push and then commit it into the repo.)

`

Ah, just realised I can build and ship a local wheel as part of the distribution:

python -m pip wheel .
mkdir -p pypi
cp jupyter_splitview*.whl pypi/

Okay - running into dependency issues arising from package versions installed in pyodide.

ValueError: Requested 'ipykernel>=6.13.0', but ipykernel==6.9.2 is already installed

I relaxed the pinning on ipykernel in the pyproject.toml file but the higher version may be being asserted elsewhere and I don't have the time to go chasing version conflicts right now.

For reference, this is the action I'm using:

name: Build and Deploy Jupyterlite Demo

on:
  push:
    branches:
      - main
  workflow_dispatch:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2
      - name: Setup Python
        uses: actions/setup-python@v2
        with:
          python-version: 3.9
      - name: Install the dependencies
        run: |
          python -m pip install -r .binder/requirements.txt
          python -m pip install -r .jupyterlite/requirements.txt
          python -m pip wheel .
          mkdir -p pypi
          cp jupyter_splitview*.whl pypi/
      - name: Build the JupyterLite site
        run: |
          mkdir -p content
          cp README.md content
          cp example_notebook.ipynb content
          jupyter lite build --contents ./content
      - name: Upload (dist)
        uses: actions/upload-artifact@v2
        with:
          name: jupyterlite-demo-dist-${{ github.run_number }}
          path: ./_output

  deploy:
    needs: [build]
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2.3.1
      - uses: actions/download-artifact@v2
        with:
          name: jupyterlite-demo-dist-${{ github.run_number }}
          path: ./dist
      - name: Deploy
        uses: JamesIves/github-pages-deploy-action@4.1.3
        with:
          branch: gh-pages
          folder: dist

It also pulls on a file .jupyterlite/requirements.txt:

#------ JupyerLite install -------
jupyterlite==0.1.0b8

#------ JupyerLab install -------
ipywidgets>=7.7,<8
jupyterlab~=3.3.0
jupyterlab-language-pack-fr-FR
jupyterlab-language-pack-zh-CN

#------- Demo install -------
# Reuse .binder/requirements.txt

You'd also need to activate Github Pages to build from root on gh-pages branch.

In the notebook, you'd need to:

import micropip
await micropip.install("jupyter-splitview")

Or in a guarded way for a generic notebook:

import platform as p

if p.machine()=="wasm32":
    import micropip
    await micropip.install("jupyter-splitview")

You should then be able to open into the demo notebook w/ a URL of the form:

https://kolibril13.github.io/jupyter-splitview/lab/index.html?path=example_notebook.ipynb

Wow, this is really exciting, thank you a lot for writing down this how-to guide, I will right now try to incorporate this into the repo.

I've now setup Jupyter lab after your draft, this would be really amazing to get working! :)
Also as expected, I get the error:
ValueError: Requested 'ipykernel>=6.13.0', but ipykernel==6.9.2 is already installed
when running

import micropip
await micropip.install("jupyter-splitview")

As an experiment, I've even fixed the kernel version here:
https://github.com/kolibril13/jupyter-splitview/blob/41948220b3768716074503b5dde46bac477d42fe/pyproject.toml#L15
but this does not change anything.
Maybe because the widget gets installed from pypi and not from the repo?
Probably something similar to this? (this does not work)

pip install git+https://github.com/kolibril13/jupyter-splitview.git@main

I'd be tempted to remove the following from the dependencies unless you are absolutely sure they are required at a particular minimum version level.

ipython = ">=7.0.0"
ipykernel = "6.9.2"

I suspect that ipython might also set up an ipykernel dependency. (Check dependencies with eg pip show ipython, though I don't know if that shows version requirements.)

I'm not sure what the Pillow version is in pyodide but that might cause an issue too. Run (help("packages") in a code cell to list installed package, although I forget whether it includes the version number.

Thanks for these instructions.
I'll try that out in the evening.
Is there any way that I can access the jupyter-splitview package from this repo from JupyterLite without downloading it from pip?
Making always a pip release for each debugging step seems to be a bit overkill.

In the action, I build a wheel and put it in the pypi directory; this is copied into the gh-pages distribution and the micropip resolver picks it up from there.

Alright, thanks for the explanation.
Later today I will try to make this work.

After one hour of tinkering, this is my update:

Thoughts:
I have no idea, why this request Requested 'ipykernel>=6.13.0' line still comes.
I am really not a version conflict expert, but maybe it's possible to solve this by not using pip, but poetry for building the package?
So here
https://github.com/kolibril13/jupyter-splitview/blob/6024553ccdebbba0ba5ac9bf39016b1024aaae79/.github/workflows/JupyterLite.yml#L23
something like this instead?
https://github.com/kolibril13/jupyter-splitview/blob/6024553ccdebbba0ba5ac9bf39016b1024aaae79/.github/workflows/pypi-publish.yml#L23-L27
Fun fact: the last few lines are from another action that generates an automatic PyPi release when I create a new tag on the main branch with the pattern 'v*..' , e.g. v.0.0.7

@psychemedia : In case that you are interested in proceeding on this (I'd be super happy if you would), I just invited you to become a collaborator in this project. For debugging pyproject.toml, the GitHub workflows and the debugging notebook, feel free to directly push on the main branch. As this project is still very small, and GitHub pages does not work nice with pull requests, developing directly on the main branch is in my opinion the fastest way to try to make this work.

The pip install route seems happy to work with the .toml file.

I don't use poetry so have no idea how it resolves things.

If you remove the dependency on ipython and ipykernel, in my local install it builds under pip without the problematically versioned ipython package, at least according to pipdeptree.

I also thought that the micropip install would pull from the pypi wheel in the JupyterLite distribution, but instead it seems to pull from PyPi. I don't have time to try to fix my misunderstanding of how that works atm - got a shed load of marking to do and a handover deadline to meet...!

Okay, then I will make one test by releasing a PyPi version without the ipython and ipykernel dependency and will just see if JupyterLite and the local installed package will still work. All the best for your deadline, and thank you so much for dedicating your time to this project, even if there are so many other things around. See you around whenever you have free time again for Jupyter widget stuff :)

Hmmm... I thought this might fix it โ€” jupyterlite/jupyterlite#556 โ€” but doesn't seem to? My build at https://ouseful-pr.github.io/jupyter-splitview/lab/index.html still loads the v7 wheel from pypi rather than the local v8 wheel I see in https://github.com/ouseful-PR/jupyter-splitview/tree/gh-pages/pypi ?

Hmm, interesting.
I've now made a new pip release with the lower requirements, and now the installation does not fail anymore (yeeey ๐ŸŽŠ! )
However, now there is a new error message when running the below cell.
@christopher-besch : Can you maybe have a quick look at this problem and see if there might be an easy fix?
To reproduce it, simply go to https://kolibril13.github.io/jupyter-splitview/lab/index.html and then start the jupyterlite_debugging_notebook.ipynb

%%splity

from skimage import data
from skimage.util import random_noise
import matplotlib.pyplot as plt

img = data.chelsea()
noisy_img = random_noise(img, var=0.02)

fig, ax1 = plt.subplots()
ax1.axis("off")
ax1.imshow(img)

fig, ax2 = plt.subplots()
ax2.axis("off")
ax2.imshow(noisy_img)
---------------------------------------------------------------------------
UndefinedError                            Traceback (most recent call last)
Input In [7], in <cell line: 1>()
----> 1 get_ipython().run_cell_magic('splity', '', '\nfrom skimage import data\nfrom skimage.util import random_noise\nimport matplotlib.pyplot as plt\n\nimg = data.chelsea()\nnoisy_img = random_noise(img, var=0.02)\n\nfig, ax1 = plt.subplots()\nax1.axis("off")\nax1.imshow(img)\n\nfig, ax2 = plt.subplots()\nax2.axis("off")\nax2.imshow(noisy_img)\n')

File /lib/python3.10/site-packages/IPython/core/interactiveshell.py:2358, in InteractiveShell.run_cell_magic(self, magic_name, line, cell)
   2356 with self.builtin_trap:
   2357     args = (magic_arg_s, cell)
-> 2358     result = fn(*args, **kwargs)
   2359 return result

File /lib/python3.10/site-packages/jupyter_splitview/sw_cellmagic.py:60, in SplitViewMagic.splity(self, line, cell)
     57 image_data_urls = [f"data:image/jpeg;base64,{base64.strip()}" for base64 in out_images_base64]
     59 # every juxtapose html node needs unique id
---> 60 inject_split(
     61     image_data_urls=image_data_urls,
     62     slider_position=slider_position,
     63     wrapper_height=int(widget_height)+4,
     64     height=int(widget_height),
     65 )

File /lib/python3.10/site-packages/jupyter_splitview/inject.py:32, in inject_split(image_data_urls, slider_position, wrapper_height, height)
     31 def inject_split(image_data_urls, slider_position, wrapper_height, height) -> None:
---> 32     html_code = compile_template(
     33         os.path.join((os.path.dirname(__file__)), "inject_split.html"),
     34         rnd_str=uuid.uuid1(),
     35         image_data_urls=image_data_urls,
     36         slider_position=slider_position,
     37         wrapper_height=wrapper_height,
     38         height=height,
     39     )
     40     display(HTML(html_code))
     41     # ensure to include the sources every time

File /lib/python3.10/site-packages/jupyter_splitview/inject.py:11, in compile_template(in_file, **variables)
      9 with open(f"{in_file}", "r", encoding="utf-8") as file:
     10     template = Template(file.read(), undefined=StrictUndefined)
---> 11 return template.render(**variables)

File /lib/python3.10/site-packages/jinja2/environment.py:1301, in Template.render(self, *args, **kwargs)
   1299     return self.environment.concat(self.root_render_func(ctx))  # type: ignore
   1300 except Exception:
-> 1301     self.environment.handle_exception()

File /lib/python3.10/site-packages/jinja2/environment.py:936, in Environment.handle_exception(self, source)
    931 """Exception handling helper.  This is used internally to either raise
    932 rewritten exceptions or return a rendered traceback for the template.
    933 """
    934 from .debug import rewrite_traceback_stack
--> 936 raise rewrite_traceback_stack(source=source)

File <template>:9, in top-level template code()

UndefinedError: list object has no element 0

I'll take a look at it. Are you sure the html files are part of the wheel? If you didn't create it with Poetry they might not be included.

I am really not sure how micropip works, but when I understand @psychemedia correctly, the micropip currently installs from pypi

My build at https://ouseful-pr.github.io/jupyter-splitview/lab/index.html still loads the [..] wheel from pypi

Therefore, I think they were build by poetry and should be included.

Ok, the problem seems to be caused by image_data_urls not having two elements. This gets caused when there aren't two images shown. I've added an error message to main that makes this clearer. But I still don't know how is is being caused by JupyterLite.

I'm having issues with setting up a dev environment for Jupyter Lite. I followed the instructions from the GitHub Action but my local changes don't get reflected when running the notebook. It seems like it always uses the version from PyPi.
I also don't understand why you aren't using poetry build to create the wheel. Wouldn't this be cleaner? And why can't poetry be used to install the dependencies as well?

Not tried this โ€” pyodide/pyodide#2731 (comment) โ€” but it suggests the wheel could be among the distributed files in the JupyterLab environment and installed from there?

I just had the same idea; I'm running into a CORS error:

ValueError: Couldn't fetch wheel from '0.0.0.0:8000/pypi/jupyter_splitview-0.0.9-py3-none-any.whl'.One common reason for this is when the server blocks Cross-Origin Resource Sharing (CORS).Check if the server is sending the correct 'Access-Control-Allow-Origin' header.

with this:

await micropip.install("/pypi/jupyter_splitview-0.0.9-py3-none-any.whl")

The weird thing is that fetch("/pypi/jupyter_splitview-0.0.9-py3-none-any.whl") runs without a problem in the browser console.
(I'm aware that that's not a very deployment-ready url.)

I am not on the computer today, but just one quick thought: Maybe the script from #4 (comment) might help for debugging the Jupiter lite demo.

I just ran that, this is the output:

out_images_base64 = []
type(data) = <class 'dict'>
type(list(data.values())[0]) = <class 'str'>
len(out_images_base64) = 0

The images are indeed missing.

That means, the images are not displayed. That was easy to fix!
I've noted that in local notebooks, creating a created figure will automatically show, in JupyterLite, a figure has to be explicitly called by plt.show().

%%splity
import matplotlib.pyplot as plt
import numpy as np

array1 = np.full((15, 30), 10)
array2 = np.random.randint(0, 10, size=(15, 30))
fig, ax1 = plt.subplots()
ax1.imshow(array1)
plt.show()
fig, ax2 = plt.subplots()
ax2.imshow(array2)
plt.show()

So this works like charm in JupyterLite.
Thanks to the two of you, for all your input and debugging efforts.
I'll update the example notebook right now.

Here is now the final JupyterLite implementation:
https://kolibril13.github.io/jupyter-splitview/

One thing I'd like to simplify:
The first cell

import micropip  #  only for JupyterLite
await micropip.install("jupyter-splitview")

could be relocated somewhere to the build process.
I've already added the dependency jupyter-splitview here:

https://github.com/kolibril13/jupyter-splitview/blob/2cc206ae933c53d8bbee4411e464014664ef7f7b/.jupyterlite/requirements.txt#L1-L2
But this did not work.
@psychemedia : Any ideas how to set up this dependency for JupyterLite? I think it's fine when the dependency is coming from PyPi, and is not build by GitHub actions from the latest main commit.

I think auto/pre-running code is still an open issue; eg jupyterlite/jupyterlite#508

Thanks for the reference. But pre-installing packages should be already possible, as also the binder requirements (NumPy, Matplotlib, etc.) are installed beforehand:

https://github.com/kolibril13/jupyter-splitview/blob/32acb75ce09d7b3af1c559f16b919fa0d25124ae/.github/workflows/JupyterLite.yml#L21

So why would that not be possible with the jupyter-splitview package?

UPDATE:
I've now experimentally added the package here, but it still does not work:

https://github.com/kolibril13/jupyter-splitview/blob/bf6e6460eef0dc7fdb42444ef4f6ee779914c2c6/.binder/requirements.txt#L4-L6

The matplotlib package is already part of the base pyodide environment and JupyterLab extensions that are installed in the host environment are also migrated into the JupyterLite distribution.

But AFAIK, arbitrary packages that are typically end user installed must still be handled manually. @jtpio could perhaps clarify the roadmap and current best practice on this.

Thanks for this clarification!
Maybe it could an idea to mention the packages from base pyodide environment at https://jupyterlite.readthedocs.io/en/latest/howto/python/packages.html

Regarding this project:
I think it's then better to remove this line:
https://github.com/kolibril13/jupyter-splitview/blob/bf6e6460eef0dc7fdb42444ef4f6ee779914c2c6/.github/workflows/JupyterLite.yml#L21
Furthermore this file can be re-arranged:
https://github.com/kolibril13/jupyter-splitview/blob/bf6e6460eef0dc7fdb42444ef4f6ee779914c2c6/.binder/requirements.txt#L1-L6
For binder, I remove the last line of binder/requirements.txt.
For JupyterLite, I remove the last and the first line binder/requirements.txt and insert it at the top of
https://github.com/kolibril13/jupyter-splitview/blob/bf6e6460eef0dc7fdb42444ef4f6ee779914c2c6/.jupyterlite/requirements.txt#L1-L2

One reason for installing via . in binder requirements is that it lets you try the latest build as per the repo, rather than the latest build as per whatever package is on PyPi. If Binder builds from PyPi, you can always get access to a runnable version of the latest dev version in.

Thanks!
It would be amazing if we could build the split-view package with GitHub Actions for JupyterLite from the repo as well!
As GithHub pages can be build from pull requests, each pull request could have their own JupyterLite environment with the corresponding package version from the pull request.
That would mean one can simultaneously compare different pull requests by opening multiple tabs in the browser and see how functionality is changing.
No more downloading the branch, opening the local IDE, starting a local JupyterServer and then start testing.
Only pressing one button and then start testing in the browser!
The GitHub action bot can even send a message into a pull request to link to the right page.
Here is a demo
Pull-request:
https://github.com/kolibril13/okapi2/pull/7
Main page:
https://kolibril13.github.io/okapi2/
Pull request demo page:
https://kolibril13.github.io/okapi2/pr-preview/pr-7/

I have no time to chase this right now, but may have a pointer on how to install packages.. https://twitter.com/martinRenou/status/1539236698068074497

The jupyterlite-xeus-python kernel allows the bundling of python packages as part of the distribution [docs].

Example automation script for producing JupyterLite distribution, including UI test regime: https://github.com/martinRenou/ipycanvas/blob/master/.github/workflows/main.yml

Thanks for the link!
With the current status of the project I am already really happy, I might come back to this in a few months when I have more time for tweaking this workflow. Maybe there's even a nice poetry workflow available by then.