.. DO NOT EDIT. .. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY. .. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE: .. "_gallery/server_browser_examples.py" .. LINE NUMBERS ARE GIVEN BELOW. .. only:: html .. note:: :class: sphx-glr-download-link-note :ref:`Go to the end ` to download the full example code. .. rst-class:: sphx-glr-example-title .. _sphx_glr__gallery_server_browser_examples.py: Serve browser examples locally. =============================== A little script to serve pygfx examples on localhost so you can try them in the browser. This is an iteration of the same script in rendercanvas and wgpu-py although some changes: * swapped in pathlib for os.path (mostly) * avoided pyscript for now, but likely a good idea to use for webworker recovery etc. Files are loaded from disk on each request, so you can leave the server running and just update examples, update pygfx and build the wheel, etc. .. GENERATED FROM PYTHON SOURCE LINES 15-338 .. code-block:: Python import os import sys import webbrowser from http.server import BaseHTTPRequestHandler, HTTPServer from pathlib import Path import flit import pygfx # from here: https://github.com/harfbuzz/uharfbuzz/pull/275 placed in /dist uharfbuzz_wheel = "uharfbuzz-0.1.dev1+ga19185453-cp310-abi3-pyodide_2025_0_wasm32.whl" # wgpu_wheel = "https://wgpu-py--753.org.readthedocs.build/en/753/_static/wgpu-0.31.0-py3-none-any.whl" # very hacky way to serve this but it does work... wgpu_wheel = "wgpu-0.31.0-py3-none-any.whl" # the pygfx wheel will be listed after this. it might be possible to still get deps from pyproject.toml pygfx_deps = [wgpu_wheel, uharfbuzz_wheel, "hsluv", "pylinalg", "jinja2", "httpx", "trimesh", "gltflib", "imageio"] root = Path(__file__).parent.parent.absolute() short_version = ".".join(str(i) for i in pygfx.version_info[:3]) wheel_name = f"pygfx-{short_version}-py3-none-any.whl" example_files = list((root / "examples").glob("**/*.py")) def get_html_index(): """Create a landing page.""" examples_list = [f"
  • {name.relative_to(root / "examples")!s}
  • " for name in example_files] html = """ pygfx browser examples Rebuild the wheel

    """ html += "List of examples that might run in Pyodide:\n" html += f"
    \n\n" html += "\n\n" return html html_index = get_html_index() # An html template to show examples using pyscript. pyscript_graphics_template = """ {example_script} via PyScript Back to list

    {docstring}

    Loading...

    """ # TODO: a pyodide example for the compute examples (so we can capture output?) # modified from _pyodide_iframe.html from rendercanvas pyodide_compute_template = """ {example_script} via Pyodide

    Loading...

    Back to list

    {docstring}

    Output:

    """ imageio_patch = """ from pyodide.http import pyfetch from pyodide.ffi import run_sync async def _imread_async(filename, **kwargs): # pyodide working version of iio.imread, uses fetch and waiting on it, also replaced the "imageio:" filename = str(filename) # maybe breaks some Path stuff? if filename.startswith("imageio:"): filename = filename.replace("imageio:", "https://raw.githubusercontent.com/imageio/imageio-binaries/master/images/") if "." in filename: kwargs["extension"] = "." + filename.rsplit(".", 1)[-1] response = await pyfetch(filename) res = iio.imread((await response.bytes()), **kwargs) return res def _imread(filename:str, **kwargs): print(f"Loading image: {filename!r} with kwargs: {kwargs}") res = run_sync(_imread_async(filename, **kwargs)) return res """ def patch_imageio_for_pyodide(python_code:str) -> str: if "iio.imread(" not in python_code: return python_code return imageio_patch + "\n\n" + python_code.replace("iio.imread(", "_imread(") def build_wheel(): toml_filename = (root / "pyproject.toml") # spews more and more with each build... might need to clear stdout or something? flit.main(["-f", str(toml_filename.resolve()), "build", "--no-use-vcs", "--format", "wheel"]) wheel_filename = root / "dist" / wheel_name assert wheel_filename.is_file(), f"{wheel_name} does not exist" # also copy the wgpu wheel if it's in a repo nearby... so make it a bit less work to update both. try: # would also be fun if this actually built the wheel too... but that might be too much atm. wgpu_wheel_filename = root.parent / "wgpu-py" / "dist" / wgpu_wheel if wgpu_wheel_filename.is_file(): # TODO: can use Pathlib copy instead? target = root / "dist" / wgpu_wheel with open(wgpu_wheel_filename, "rb") as src, open(target, "wb") as dst: dst.write(src.read()) print(f"Copied {wgpu_wheel} to dist folder.") else: print(f"{wgpu_wheel} not found in nearby repo, skipping copy.") except Exception as e: print(f"Error copying {wgpu_wheel}: {e}") def get_docstring_from_py_file(fname): filename = root / "examples" / fname docstate = 0 doc = "" with open(filename, "rb") as f: for line in f: line = line.decode() if docstate == 0: if line.lstrip().startswith('"""'): docstate = 1 else: if docstate == 1 and line.lstrip().startswith(("---", "===")): docstate = 2 doc = "" elif '"""' in line: doc += line.partition('"""')[0] break else: doc += line return doc.replace("\n", "
    ") class MyHandler(BaseHTTPRequestHandler): def do_GET(self): if self.path == "/": self.respond(200, html_index, "text/html") elif self.path == "/build": try: self.respond(200, "Building wheel...
    ", "text/html") build_wheel() except Exception as err: self.respond(500, str(err), "text/plain") else: html = f"
    Wheel build: {wheel_name}

    Back to list" self.respond(200, html, "text/html") elif self.path.endswith(".whl"): requested_path = Path(self.path) filename = root / "dist" / requested_path.name if os.path.isfile(filename): with open(filename, "rb") as f: data = f.read() self.respond(200, data, "application/octet-stream") else: self.respond(404, "wheel not found") elif self.path.endswith(".html"): # name = self.path.strip("/") pyname = self.path.replace(".html", ".py").lstrip("/") try: doc = get_docstring_from_py_file(pyname) deps = [*pygfx_deps, f"/{wheel_name}"] html = pyodide_compute_template.format( docstring=doc, example_script=pyname, # todo: refactor this to a list and maybe get other deps from pyodide.loadPackagesFromImports dependencies="\n".join( [f"await micropip.install({dep!r});" for dep in deps] ), ) self.respond(200, html, "text/html") except Exception as err: self.respond(404, f"example not found: {err}") elif self.path.endswith(".py"): filename = os.path.join(root, "examples", self.path.strip("/")) if os.path.isfile(filename): with open(filename, "rb") as f: data = f.read() data = patch_imageio_for_pyodide(data.decode()).encode() self.respond(200, data, "text/plain") else: self.respond(404, "py file not found") # TODO: we could try to mount a virtual filesystem and fill it... but I think using fetch and serving the files could work easier. elif self.path.startswith("/home/data/"): requested_path = Path(self.path) # this is for the pyodide examples that need to load data files - we mount the examples/data folder to /home/data in pyodide, so we can serve those files here. filename = root / "examples" / "data" / requested_path.relative_to("/home/data") if os.path.isfile(filename): with open(filename, "rb") as f: data = f.read() self.respond(200, data, "application/octet-stream") else: self.respond(404, "data file not found") else: self.respond(404, "not found") def respond(self, code, body, content_type="text/plain"): self.send_response(code) self.send_header("Content-type", content_type) self.send_header("Access-Control-Allow-Origin", "*") self.send_header("Access-Control-Allow-Methods", "GET, OPTIONS") self.send_header("Access-Control-Allow-Headers", "Content-Type") self.end_headers() if isinstance(body, str): body = body.encode() self.wfile.write(body) if __name__ == "__main__": port = 8000 if len(sys.argv) > 1: try: port = int(sys.argv[-1]) except ValueError: pass build_wheel() print("Opening page in web browser ...") webbrowser.open(f"http://localhost:{port}/") HTTPServer(("", port), MyHandler).serve_forever() .. _sphx_glr_download__gallery_server_browser_examples.py: .. only:: html .. container:: sphx-glr-footer sphx-glr-footer-example .. container:: sphx-glr-download sphx-glr-download-jupyter :download:`Download Jupyter notebook: server_browser_examples.ipynb ` .. container:: sphx-glr-download sphx-glr-download-python :download:`Download Python source code: server_browser_examples.py ` .. container:: sphx-glr-download sphx-glr-download-zip :download:`Download zipped: server_browser_examples.zip ` .. only:: html .. rst-class:: sphx-glr-signature `Gallery generated by Sphinx-Gallery `_ .. only:: html Interactive example ------------------- Try this example in your browser using Pyodide. Might not work with all examples and all devices. Check the output and your browser's console for details. .. raw:: html