Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

VS Code: Failed to load model class 'AnyModel' from module 'anywidget' #684

Open
basnijholt opened this issue Sep 14, 2024 · 3 comments
Open
Labels
bug Something isn't working

Comments

@basnijholt
Copy link

basnijholt commented Sep 14, 2024

Describe the bug

First of all, thanks for this awesome package! It has made developing a widget for my package (pipefunc/pipefunc#323) much easier!

My widget works in a Jupyter notebook but in VS Code I get:

[Open Browser Console for more detailed log - Double click to close this message]
Failed to load model class 'AnyModel' from module 'anywidget'
Error: No version of module anywidget is registered
    at ph.loadClass (https://file+.vscode-resource.vscode-cdn.net/Users/basnijholt/.vscode/extensions/ms-toolsai.jupyter-renderers-1.0.19/out/node_modules/%40vscode/jupyter-ipywidgets8/dist/ipywidgets.js:2:4099813)
    at ph.loadClass (https://file+.vscode-resource.vscode-cdn.net/Users/basnijholt/.vscode/extensions/ms-toolsai.jupyter-renderers-1.0.19/out/node_modules/%40vscode/jupyter-ipywidgets8/dist/ipywidgets.js:2:4403287)
    at ph.loadModelClass (https://file+.vscode-resource.vscode-cdn.net/Users/basnijholt/.vscode/extensions/ms-toolsai.jupyter-renderers-1.0.19/out/node_modules/%40vscode/jupyter-ipywidgets8/dist/ipywidgets.js:2:4097773)
    at ph._make_model (https://file+.vscode-resource.vscode-cdn.net/Users/basnijholt/.vscode/extensions/ms-toolsai.jupyter-renderers-1.0.19/out/node_modules/%40vscode/jupyter-ipywidgets8/dist/ipywidgets.js:2:4094616)
    at ph.new_model (https://file+.vscode-resource.vscode-cdn.net/Users/basnijholt/.vscode/extensions/ms-toolsai.jupyter-renderers-1.0.19/out/node_modules/%40vscode/jupyter-ipywidgets8/dist/ipywidgets.js:2:4092246)
    at ph.handle_comm_open (https://file+.vscode-resource.vscode-cdn.net/Users/basnijholt/.vscode/extensions/ms-toolsai.jupyter-renderers-1.0.19/out/node_modules/%40vscode/jupyter-ipywidgets8/dist/ipywidgets.js:2:4091039)
    at https://file+.vscode-resource.vscode-cdn.net/Users/basnijholt/.vscode/extensions/ms-toolsai.jupyter-renderers-1.0.19/out/node_modules/%40vscode/jupyter-ipywidgets8/dist/ipywidgets.js:2:4402511
    at n._handleCommOpen (https://file+.vscode-resource.vscode-cdn.net/Users/basnijholt/.vscode/extensions/ms-toolsai.jupyter-2024.8.1-darwin-arm64/dist/webviews/webview-side/ipywidgetsKernel/ipywidgetsKernel.js:3:80955)
    at async n._handleMessage (https://file+.vscode-resource.vscode-cdn.net/Users/basnijholt/.vscode/extensions/ms-toolsai.jupyter-2024.8.1-darwin-arm64/dist/webviews/webview-side/ipywidgetsKernel/ipywidgetsKernel.js:3:82830)

I found some related issue here:

Reproduction

I have this widget

from pathlib import Path
from typing import Any

import anywidget
import ipywidgets as widgets
import traitlets


class PipeFuncGraphWidget(anywidget.AnyWidget):
    """A widget for rendering a graphviz graph using d3-graphviz.

    Example:
    -------
    >>> dot_string = "digraph { a -> b; b -> c; c -> a; }"
    >>> pipe_func_graph_widget = PipeFuncGraphWidget(dot_source=dot_string)
    >>> pipe_func_graph_widget

    """

    _esm = Path(__file__).parent / "static" / "graphviz_widget.js"

    _css = """
    #graph {
        margin: auto;
    }
    """

    dot_source = traitlets.Unicode("").tag(sync=True)
    selected_direction = traitlets.Unicode("bidirectional").tag(sync=True)


def graph_widget(dot_string: str = "digraph { a -> b; b -> c; c -> a; }") -> widgets.VBox:
    pipe_func_graph_widget = PipeFuncGraphWidget(dot_source=dot_string)
    reset_button = widgets.Button(description="Reset Zoom")
    direction_selector = widgets.Dropdown(
        options=["bidirectional", "downstream", "upstream", "single"],
        value="bidirectional",
        description="Direction:",
    )

    # Define button actions
    def reset_graph(_: Any) -> None:
        pipe_func_graph_widget.send({"action": "reset_zoom"})

    def update_direction(change: dict) -> None:
        pipe_func_graph_widget.selected_direction = change["new"]

    reset_button.on_click(reset_graph)
    direction_selector.observe(update_direction, names="value")

    # Display widgets
    return widgets.VBox(
        [
            widgets.HBox([reset_button, direction_selector]),
            pipe_func_graph_widget,
        ],
    )

with graphviz_widget.js:

function loadScript(url) {
    return new Promise((resolve, reject) => {
        const script = document.createElement("script");
        script.src = url;
        script.onload = () => resolve();
        script.onerror = () => reject(new Error(`Failed to load script: ${url}`));
        document.head.append(script);
    });
}

async function render({ model, el }) {
    // Load scripts in order
    await loadScript("https://unpkg.com/[email protected]/dist/jquery.min.js");
    await loadScript("https://unpkg.com/[email protected]/jquery.mousewheel.js");
    await loadScript("https://unpkg.com/[email protected]/dist/jquery.color.js");
    await loadScript("https://unpkg.com/[email protected]/dist/d3.min.js");
    await loadScript("https://cdn.jsdelivr.net/gh/mountainstorm/jquery.graphviz.svg@master/js/jquery.graphviz.svg.js");
    await loadScript("https://unpkg.com/@hpcc-js/[email protected]/dist/index.min.js");
    await loadScript("https://unpkg.com/[email protected]/build/d3-graphviz.min.js");

    const $ = window.jQuery;
    const d3 = window.d3;

    // Prepare the graph container
    el.innerHTML = '<div id="graph" style="text-align: center;"></div>';
    const graphEl = $(el).find('#graph');

    // Initialize a d3-graphviz renderer instance
    var graphviz = d3.select("#graph").graphviz();

    // Configuration for transitions in rendering the graph
    var d3Config = {
        transitionDelay: 0,
        transitionDuration: 500
    };

    // Variable for storing the selected graph rendering engine
    var selectedEngine = "dot";

    // Object for saving the current GraphVizSVG
    var graphVizObject;

    // Variable for storing the selected direction for highlighting
    var selectedDirection = model.get("selected_direction") || "bidirectional";

    // Array holding the current selections
    var currentSelection = [];

    // Function to highlight selected nodes and their connected nodes
    function highlightSelection() {
        let highlightedNodes = $();
        currentSelection.forEach(selection => {
            const nodes = getConnectedNodes(selection.set, selection.direction);
            highlightedNodes = highlightedNodes.add(nodes);
        });
        graphVizObject.highlight(highlightedNodes, true);
    }

    // Function to retrieve nodes connected in the specified direction
    function getConnectedNodes(nodeSet, mode = "bidirectional") {
        let resultSet = $().add(nodeSet);
        const nodes = graphVizObject.nodesByName();

        nodeSet.each((i, el) => {
            if (el.className.baseVal === "edge") {
                const [startNode, endNode] = $(el).data("name").split("->");
                if ((mode === "bidirectional" || mode === "upstream") && startNode) {
                    resultSet = resultSet.add(nodes[startNode]).add(graphVizObject.linkedTo(nodes[startNode], true));
                }
                if ((mode === "bidirectional" || mode === "downstream") && endNode) {
                    resultSet = resultSet.add(nodes[endNode]).add(graphVizObject.linkedFrom(nodes[endNode], true));
                }
            } else {
                if (mode === "bidirectional" || mode === "upstream") {
                    resultSet = resultSet.add(graphVizObject.linkedTo(el, true));
                }
                if (mode === "bidirectional" || mode === "downstream") {
                    resultSet = resultSet.add(graphVizObject.linkedFrom(el, true));
                }
            }
        });
        return resultSet;
    }

    // Function to reset the graph zoom and selection highlights
    function resetGraph() {
        graphviz.resetZoom();
        graphVizObject.highlight(); // Reset node selection on reset
        currentSelection = [];
    }

    // Function to update the selected direction for highlighting
    function updateDirection(newDirection) {
        selectedDirection = newDirection;
        resetGraph();
    }

    // Main function to render the graph from DOT source
    function render(dotSource) {
        var transition = d3.transition("graphTransition")
            .ease(d3.easeLinear)
            .delay(d3Config.transitionDelay)
            .duration(d3Config.transitionDuration);

        graphviz
            .engine(selectedEngine)
            .fade(true)
            .transition(transition)
            .tweenPaths(true)
            .tweenShapes(true)
            .zoomScaleExtent([0, Infinity])
            .zoom(true)
            .renderDot(dotSource)
            .on("end", function () {
                // Calls the jquery.graphviz.svg setup directly
                $('#graph').data('graphviz.svg').setup();  // Re-setup after rendering
            });
    }

    // Document ready function
    $(document).ready(function () {
        // Initialize the GraphVizSVG object from jquery.graphviz.svg.js
        $("#graph").graphviz({
            shrink: null,
            zoom: false,
            ready: function () {
                graphVizObject = this;

                // Event listener for node clicks to handle selection
                graphVizObject.nodes().click(function (event) {
                    const nodeSet = $().add(this);
                    const selectionObject = {
                        set: nodeSet,
                        direction: selectedDirection
                    };

                    // If CMD, CTRL, or SHIFT is pressed, add to the selection
                    if (event.ctrlKey || event.metaKey || event.shiftKey) {
                        currentSelection.push(selectionObject);
                    } else {
                        currentSelection = [selectionObject];
                    }

                    highlightSelection();
                });
                // Event listener for pressing the escape key to cancel highlights
                $(document).keydown(function (event) {
                    if (event.keyCode === 27) {
                        graphVizObject.highlight();
                    }
                });
            }
        });
    });

    // Event listeners for `anywidget` events
    model.on("change:dot_source", () => {
        render(model.get("dot_source"));
    });

    model.on("change:selected_direction", () => {
        updateDirection(model.get("selected_direction"));
    });

    model.on("msg:custom", (msg) => {
        if (msg.action === "reset_zoom") {
            resetGraph();
        }
    });

    render(model.get("dot_source"));
}
export default { render };

The I run:

graph_widget()

In a Jupyter notebook in the browser this works, but in VS Code I see the error I posted above.

Logs

No response

System Info

at 16:44:43 ❯ pip freeze | grep -E "anywidget|jupyter|notebook"
anywidget @ file:///home/conda/feedstock_root/build_artifacts/anywidget_1719028522224/work
jupyter-cache==1.0.0
jupyter-events @ file:///home/conda/feedstock_root/build_artifacts/jupyter_events_1710805637316/work
jupyter-lsp @ file:///home/conda/feedstock_root/build_artifacts/jupyter-lsp-meta_1712707420468/work/jupyter-lsp
jupyter_client @ file:///home/conda/feedstock_root/build_artifacts/jupyter_client_1716472197302/work
jupyter_core @ file:///Users/runner/miniforge3/conda-bld/jupyter_core_1710257589360/work
jupyter_server @ file:///home/conda/feedstock_root/build_artifacts/jupyter_server_1720816649297/work
jupyter_server_terminals @ file:///home/conda/feedstock_root/build_artifacts/jupyter_server_terminals_1710262634903/work
jupyterlab @ file:///home/conda/feedstock_root/build_artifacts/jupyterlab_1724745148804/work
jupyterlab_pygments @ file:///home/conda/feedstock_root/build_artifacts/jupyterlab_pygments_1707149102966/work
jupyterlab_server @ file:///home/conda/feedstock_root/build_artifacts/jupyterlab_server-split_1721163288448/work
jupyterlab_widgets @ file:///home/conda/feedstock_root/build_artifacts/jupyterlab_widgets_1724331334887/work
notebook @ file:///home/conda/feedstock_root/build_artifacts/notebook_1724861303656/work
notebook_shim @ file:///home/conda/feedstock_root/build_artifacts/notebook-shim_1707957777232/work


### Severity

blocking all usage of anywidget
@basnijholt basnijholt added the bug Something isn't working label Sep 14, 2024
@basnijholt
Copy link
Author

Might be related: microsoft/vscode-jupyter#15406

@manzt
Copy link
Owner

manzt commented Oct 1, 2024

Thanks for the detailed error message. I'm sorry I haven't been able to dive deep into this yet. Some users on Discord mentioned some issues in an update of VS Code and I'm wondering if they have been resolved now.

@manzt
Copy link
Owner

manzt commented Oct 1, 2024

I just tested locally with the anywidget template repo and I'm able to develop in VS Code:

pnpm create anywidget@latest

However, I think there may be an issue with the global environment. I see the graph_widget.js code appends scripts to the window. In my experience, this is a very unreliable way to load javascript code as there can be version conflicts or the UMD builds of those pacakges will look for tools like RequireJS (which is a primary source of pain and variation between notebook frontnends). In order to make anywidget work, we focused on ECMAScript modules and hope widget authors will also use ESM since it's much more reliable way to JavaScript in an isolated manner.

I'm not sure whether the packages you've included have been packaged as ESM anywhere via a CDN (I'd try something like esm.sh), but otherwise you can use the project template and try bundling them locally with esbuild.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants