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

Add style checks and the minimal plugins UI #35

Merged
merged 16 commits into from
May 24, 2023
Merged

Conversation

juntyr
Copy link
Collaborator

@juntyr juntyr commented May 12, 2023

  • Adds style checks to CI with black, isort, and flake8

  • Fixes any style issues

  • Implements plugin loading into the playground

TODOs:

  • fix async install on WASM
  • reload the "loaded plugins" list
  • reload read plugins
  • reload write plugins
  • reload plot plugins
  • reload global plugins
  • reload callback plugins

Future work:

  • also support uploading plugins
  • add list of suggested plugins
  • give hints for loading plots from not-yet-installed plugins
  • add custom URLs to install plugins upon page-load in the WASM version

@juntyr juntyr self-assigned this May 12, 2023
@juntyr juntyr marked this pull request as draft May 12, 2023 13:47
@Aggrathon
Copy link
Member

@Aggrathon Should plugin installs outside WASM be supported at all, since we cannot be sure that the user and host are the same person? Here it might be better to just error and say to use pip install or the WASM version instead.

I agree, installing plugins from the UI should be a WASM only thing

@Aggrathon
Copy link
Member

@Aggrathon How should plugins be handled with the downloads? Do we enforce that all plugins must be uploaded as .whl files and then provide a new download option with them included and install them (with a warning emitted for each) on data upload? If we don't enforce it, how do we get the wheels for any pre-installed packages / packages installed from PyPi / from a URL?

I think we shouldn't add the plugins to the downloads. It is up to the user to remember to install all necessary plugins (although we should add error popups for missing plot types etc.). And if the user forgets, then they can just reupload the saved state after installing the necessary plugins.

@Aggrathon
Copy link
Member

Aggrathon commented May 12, 2023

@Aggrathon What should we do about plugin reinstalls? Can we detect them in pip and micropip and error since we won't be able to re-add the plugin hooks

micropip correctly adds all entrypoints when installing packages. So all we have to do is to a) recreate the layout (app.layout) and b) register the new callbacks (but only the new ones). Maybe it would be easier to restart the dash server instead of keeping track of all this? As for pip, I don't think we should support (other than accidentally) installing plugins while the server is running.

@juntyr
Copy link
Collaborator Author

juntyr commented May 12, 2023

@Aggrathon What should we do about plugin reinstalls? Can we detect them in pip and micropip and error since we won't be able to re-add the plugin hooks

micropip correctly adds all entrypoints when installing packages. So all we have to do is to a) recreate the layout (app.layout) and b) register the new callbacks (but only the new ones). Maybe it would be easier to restart the dash server instead of keeping track of all this? As for pip, I don't think we should support (other than accidentally) installing plugins while the server is running.

That would be an option … I’m not sure how easy it is to restart plotly and a dash server but we could try that. If so, there should be an option to install many plugins, list the ones which are installed but not loaded, and then provide a big red button with a warning confirmation message to reaload, but with the recommendation to download any plots first.

However, supporting dynamically loading in plugins might be a bit neater and provide a nicer user experience

@juntyr
Copy link
Collaborator Author

juntyr commented May 12, 2023

@Aggrathon How should plugins be handled with the downloads? Do we enforce that all plugins must be uploaded as .whl files and then provide a new download option with them included and install them (with a warning emitted for each) on data upload? If we don't enforce it, how do we get the wheels for any pre-installed packages / packages installed from PyPi / from a URL?

I think we shouldn't add the plugins to the downloads. It is up to the user to remember to install all necessary plugins (although we should add error popups for missing plot types etc.). And if the user forgets, then they can just reupload the saved state after installing the necessary plugins.

Ok, we can do that. In that case, it might be nice to still remember the list of loaded plot plugins so that if there is an error loading a plot we can point to the plugin that is missing.

@juntyr
Copy link
Collaborator Author

juntyr commented May 15, 2023

@Aggrathon @TanakaAkihiro I've now finished a minimal implementation that I'd like to land so that the remaining work can be distributed.

What works:

  • plugins can be installed inside WASM
  • installation has a progress, success, and failure notification
  • installation can happen from bundled plugins, PyPi, and .whl URLs
  • the UI is updated for new plugins:
    • lists all installed plugins
    • makes all plugins (read, write, global, callback, plot) available
    • plugin overwrites are simply not executed
  • there some is protection against concurrent and double installs
  • everything is disabled in the non-WASM version, here only installed plugins are listed

What is missing but should be implemented in follow-up PRs (and probably not by me since I won't have the time):

  • uploading plugins: this should be easy since it could use most of the code from uploading datasets. One problem will be the global callback for dash_uploader. The uploaded plugins could be put into the plugins folder and then a message could pop up to suggest selecting them in the dropdown to install them next. This could also be automated if you'd like to do that.
  • listing suggested plugins: right now 'suggested' plugins are bundled. This could be changed to some metadata file that simply lists the plugin names and their installation paths. This list should be parsed into the dropdown options, and once an item has been installed successfully, it should be removed from the list. Once we have ported some plots and nice extra libraries to their separate PyPi packages, bundling could be removed. However, it could still be nice for development purposes.
  • warning about uninstalled plugin plots: right now loading a dataset with unknown plot types will error. The download format could store wether a plot came from a plugin and if so give a warning instead that suggests installing the plugin

Since all of these are quality-of-life improvements but don't include any required functionality, I'd propose merging this PR and #34.

@juntyr juntyr marked this pull request as ready for review May 15, 2023 13:11
@juntyr juntyr requested review from Aggrathon and TanakaAkihiro May 15, 2023 13:11
@Aggrathon
Copy link
Member

  • I don't think the plugin tab should be visible in the non-WASM version. It takes too much space for something that is not working. I understand the "advertising" value, but since it is a new top-level tab (instead of e.g. being part of the settings tab) it is too visible.
  • On narrower window sizes the plugin instructions get cut off. Maybe add a hovertext or something.
  • The list of installed plugins is functional, but ugly. Currently an item is [hook_type] entry-point-key: entry-point-target. A simple improvement would be to remove either the entry-point-key or the entry-point-target. But ideally I would suggest only showing the package name / .whl name / .whl url.
  • If uploading is not working, just hide the box.
  • I tried installing a package that is not a plugin, but adds functionality, namely fastparquet. And I'm happy to report that the UI is correctly updated and the loading of .parquet files works as expected. However, it is not added to the list of plugins and trying to save .parquet files raises an IOError (complains about "file closed" for some reason?), but this is maybe for a future bugfix.
  • Suggestion for future work: What if we could construct links like https://edahelsinki.fi/xiplot?plugin=package_a&plugin=package_b that would automatically load the specified plugins?

@juntyr
Copy link
Collaborator Author

juntyr commented May 16, 2023

  • I don't think the plugin tab should be visible in the non-WASM version. It takes too much space for something that is not working. I understand the "advertising" value, but since it is a new top-level tab (instead of e.g. being part of the settings tab) it is too visible.

That is a possibility and would simplify some code, though it would still be nice to have a list of loaded plugins in the non-WASM version.

* If uploading is not working, just hide the box.

If we hide the plugins in non-WASM, this issue is resolved in my opinion. We should provide uploads in WASM unconditionally, but not hide them right now because it serves as an implementation reminder.

* On narrower window sizes the plugin instructions get cut off. Maybe add a hovertext or something.

Sounds good to me.

* The list of installed plugins is functional, but ugly. Currently an item is `[hook_type] entry-point-key: entry-point-target`. A simple improvement would be to remove either the `entry-point-key` or the `entry-point-target`. But ideally I would suggest only showing the _package name / .whl name / .whl url_.
* ... However, it is not added to the list of plugins ...

This is a fundamental limitation of our plugin discovery at the moment. We only know about plugins that provide us with the known xiplot hooks. While we could list every installed PyPi plugin, I don't think this would be useful. What could be done is to list all entry point targets, derive the package names from that, and then list package name: (hook-type),*

* I tried installing a package that is not a plugin, but adds functionality, namely `fastparquet`. And I'm happy to report that the UI is correctly updated and the loading of .parquet files works as expected. However, it is not added to the list of plugins and trying to save .parquet files raises an IOError (complains about "file closed" for some reason?), but this is maybe for a future bugfix.

I don't like that this is effectively a silent plugin at the moment, i.e. installing it has an effect but we cannot detect really detect it, unless we special-case the plugin discovery to check for parquet support. I'd rather us publish a wrapper plugin that just imports the dependency. The bug fix is out of scope right now.

* Suggestion for future work: What if we could construct links like https://edahelsinki.fi/xiplot?plugin=package_a&plugin=package_b that would automatically load the specified plugins?

Sounds good to me, out of scope for now.

@Aggrathon
Copy link
Member

Aggrathon commented May 16, 2023

That is a possibility and would simplify some code, though it would still be nice to have a list of loaded plugins in the non-WASM version.

A list of installed plugins in the non-WASM version is a nice touch. However, personally I don't think it is that valuable, especially if removing it would lead to improved UI and, possibly, a better list for the WASM version.

@juntyr
Copy link
Collaborator Author

juntyr commented May 16, 2023

That is a possibility and would simplify some code, though it would still be nice to have a list of loaded plugins in the non-WASM version.

A list of installed plugins in the non-WASM version is a nice touch. However, personally I don't think it is that valuable, especially if removing it would lead to improved UI and, possibly, a better list for the WASM version.

I've now moved the list to the settings menu if the plugins tab is not loaded because loading new plugins is unsupported. The problem with a better plugin list, which includes installed packages that don't register any hooks, is that we'd have to diff the micropip list after an install from before the launch to get all packages. This would include the lazy sklearn load, and any dependencies any plugin might have. I think it would be far too cluttered too quickly.

@juntyr
Copy link
Collaborator Author

juntyr commented May 16, 2023

The bug is in fastparquet, but I added a workaround for it. Essentially, I subclass BytesIO to disallow it from being closed while the df is written to it. Afterwards, I manually do the cleanup with a context helper class. It seems to work.

@Aggrathon
Copy link
Member

The bug is in fastparquet, but I added a workaround for it. Essentially, I subclass BytesIO to disallow it from being closed while the df is written to it. Afterwards, I manually do the cleanup with a context helper class. It seems to work.

Oh, that ended up being more convoluted than expected (and in hindsight maybe not that necessary to work around). But the solution you found is pretty cool!

@Aggrathon
Copy link
Member

I've now moved the list to the settings menu if the plugins tab is not loaded because loading new plugins is unsupported. The problem with a better plugin list, which includes installed packages that don't register any hooks, is that we'd have to diff the micropip list after an install from before the launch to get all packages. This would include the lazy sklearn load, and any dependencies any plugin might have. I think it would be far too cluttered too quickly.

Leaving improvements to the plugin list for future work is okay, but it already looks pretty great (one nitpick would be to parse the path with path.split(":")[0].split(".")[0] otherwise the Slisemap plugin shows up as slisemap_interactive.xiplot).

What I would have done is have a dumb list that gets appended to whenever the user clicks "install" (after a successful install just add whatever was in the input, therefore ignoring dependencies), but this would not work for the non-WASM list.

@juntyr
Copy link
Collaborator Author

juntyr commented May 17, 2023

@Aggrathon @TanakaAkihiro This PR is now ready for final review and to be merged.

Copy link
Collaborator

@TanakaAkihiro TanakaAkihiro left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@Aggrathon
Copy link
Member

I tried having two "recommended" plugins in the WASM branch, but as soon as I loaded one of them the list of recommended plugins was cleared. This is not a show stopping bug (since we don't have multiple recommended plugins) but it should at least be mentioned for future work.

@juntyr
Copy link
Collaborator Author

juntyr commented May 17, 2023

I tried having two "recommended" plugins in the WASM branch, but as soon as I loaded one of them the list of recommended plugins was cleared. This is not a show stopping bug (since we don't have multiple recommended plugins) but it should at least be mentioned for future work.

That is a bug. In the process I discovered another issue and fixed both. Thanks for catching it! I also renamed the dir_path variable to data_dir.

@Aggrathon
Copy link
Member

I've found a major issue. Any dynamically added callback is not registered on the client side. Meaning any plugin type requiring callbacks, such as plots, do not work. Below is a simple modification to the test plugin that should demonstrate this:

import pandas as pd
from dash import html, Input, Output, MATCH


def plugin_load():
    def read(data):
        print("Reading test data")
        return pd.DataFrame({"x": [1, 2, 3]})

    return read, ".test"


def plugin_write():
    def write(df, file):
        print("Writing test data")

    return write, ".test", "example/none"


def create_global():
    return html.Div("TEST PLUGIN", style={"display": "none"}, id="test-plugin-global")


def register_callbacks(app, df_from_store, df_to_store):
    print("The test-plugin has registered a callback")


class Plot:
    @classmethod
    def name(cls) -> str:
        return "  TEST PLUGIN"

    @staticmethod
    def register_callbacks(app, df_from_store, df_to_store):
        @app.callback(
            Output({"index": MATCH, "type": "test_counter"}, "children"),
            Input({"index": MATCH, "type": "test_button"}, "n_clicks"),
        )
        def counter(num):
            return f"{num} clicks"

        print("The test-plugin plot has registered a callback")

    @staticmethod
    def create_new_layout(index, df, columns, config=dict()):
        return html.Div(
            [
                html.Button(["Test Plot"], id={"index": index, "type": "test_button"}),
                html.Span(["No clicks"], id={"index": index, "type": "test_counter"}),
            ]
        )

@juntyr
Copy link
Collaborator Author

juntyr commented May 20, 2023

@Aggrathon Well dang. I tried for a while to get it to work, but it doesn't (the problem is that the frontend would need to refetch the "/_dash-dependencies" endpoint to get the new callbacks - I have no idea how to force it to do that though). I have figured out a way to do the page reload though. So my suggestion is to remove all dynamic plugin update code, to track which plugins have been loaded since the last reload and are thus not accessible yet, and to give a big reload button for them. We should probably also disallow the read and write plugins from working immediately, just for consistency.

@juntyr
Copy link
Collaborator Author

juntyr commented May 20, 2023

I'll implement this next week.

@juntyr
Copy link
Collaborator Author

juntyr commented May 22, 2023

@Aggrathon I've now implemented the manual reload variant and stripped out the code for the dynamic plugin loads. I hope that this version is now ready to be merged.

Copy link
Member

@Aggrathon Aggrathon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! (even without my latest suggestions)

@juntyr juntyr merged commit c76fe5e into main May 24, 2023
@juntyr juntyr deleted the minimal-plugins-ui branch May 24, 2023 15:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants