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

feat: Add plotly-express JsPlugin implementation and registration #150

Merged
merged 8 commits into from
Dec 20, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions plugins/plotly-express/setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name = deephaven-plugin-plotly-express
description = Deephaven Chart Plugin
long_description = file: README.md
long_description_content_type = text/markdown
version = attr:deephaven.plot.express.__version__
version = 0.2.0.dev1
url = https://github.com/deephaven/deephaven-plugins
project_urls =
Source Code = https://github.com/deephaven/deephaven-plugins
Expand Down Expand Up @@ -34,4 +34,4 @@ where=src

[options.entry_points]
deephaven.plugin =
registration_cls = deephaven.plot.express:ChartRegistration
registration_cls = deephaven.plot.express:ExpressRegistration
7 changes: 4 additions & 3 deletions plugins/plotly-express/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
from setuptools import setup
import os
import subprocess
import json

# npm pack in js directory

Expand All @@ -18,11 +17,13 @@
os.makedirs(dest_dir, exist_ok=True)

# pack and unpack into the js plotly-express directory
subprocess.run(["npm", "pack", "--pack-destination", project], cwd=js_dir)
subprocess.run(
["npm", "pack", "--pack-destination", project], cwd=js_dir, check=True
)
# it is assumed that there is only one tarball in the directory
files = os.listdir(dest_dir)
for file in files:
subprocess.run(["tar", "-xzf", file], cwd=dest_dir)
subprocess.run(["tar", "-xzf", file], cwd=dest_dir, check=True)
os.remove(os.path.join(dest_dir, file))

# move the contents of the package directory to the plotly-express directory
Expand Down
9 changes: 5 additions & 4 deletions plugins/plotly-express/src/deephaven/plot/express/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from .communication.DeephavenFigureConnection import DeephavenFigureConnection
from .deephaven_figure import DeephavenFigure
from .js import create_js_plugin

from .plots import (
area,
Expand Down Expand Up @@ -44,7 +45,6 @@

from .data import data_generators

__version__ = "0.2.0dev1"

NAME = "deephaven.plot.express.DeephavenFigure"

Expand Down Expand Up @@ -103,20 +103,21 @@ def create_client_connection(
return figure_connection


class ChartRegistration(Registration):
class ExpressRegistration(Registration):
Copy link
Member

Choose a reason for hiding this comment

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

This might be a bigger refactor than originally planned, but I'm not sure this belongs here (nor does the new import create_js_plugin).

The expectation (and documentation) is that users start with:

import deephaven.plot.express as dx

but with code as it stands, dx. will now include ExpressRegistration and create_js_plugin.

As long as the registration is being renamed (an internal type, won't affect any downstream users), I'd suggest it belongs in its own file to avoid this.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, fair enough - should be "internal" code.

Copy link
Member Author

Choose a reason for hiding this comment

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

I've refactored so JS / registration are internal. It might be reasonable to refacter DeephavenFigureType in the same way.

"""
Register the DeephavenFigureType
Register the DeephavenFigureType and ExpressJsPlugin

"""

@classmethod
def register_into(cls, callback: Callback) -> None:
"""
Register the DeephavenFigureType
Register the DeephavenFigureType and ExpressJsPlugin

Args:
callback: Registration.Callback:
A function to call after registration

"""
callback.register(DeephavenFigureType)
callback.register(create_js_plugin())
76 changes: 76 additions & 0 deletions plugins/plotly-express/src/deephaven/plot/express/js/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import typing
import pathlib

import importlib.metadata
import importlib.resources
import json
import sys
from typing import Callable

from deephaven.plugin.js import JsPlugin


class ExpressJsPlugin(JsPlugin):
def __init__(
self,
name: str,
version: str,
main: str,
root_provider: Callable[[], typing.Generator[pathlib.Path, None, None]],
) -> None:
self._name = name
self._version = version
self._main = main
self._root_provider = root_provider

@property
def name(self) -> str:
return self._name

@property
def version(self) -> str:
return self._version

@property
def main(self) -> str:
return self._main

def distribution_path(self) -> typing.Generator[pathlib.Path, None, None]:
# TODO: Finalize JsPlugin
# https://github.com/deephaven/deephaven-plugin/issues/15
return self._root_provider()


def _create_from_npm_package_json(
root_provider: Callable[[], typing.Generator[pathlib.Path, None, None]]
) -> JsPlugin:
with root_provider() as root, (root / "package.json").open("rb") as f:
package_json = json.load(f)
Copy link
Member

Choose a reason for hiding this comment

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

Am I missing something here? You're trying to use root_provider() as a context manager, but it returns a Generator so this is just going to be an error

Copy link
Member Author

Choose a reason for hiding this comment

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

So, this code "works" - I don't think it's just an artifact of python duck typing; I think the code is technically correct, but the typing may be more general than we need it to be. I think when I was originally writing this code, I saw a suggestion somewhere that ContextManager should use typing.Generator typing. I'll look to see if there is more explicit typing suggestions for ContextManager...

Copy link
Member Author

Choose a reason for hiding this comment

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

At least the "technically correct", which may have lead me or others to using Generator typeing, https://docs.python.org/3.11/library/contextlib.html#contextlib.contextmanager:

The function being decorated must return a generator-iterator when called. This iterator must yield exactly one value, which will be bound to the targets in the with statement’s as clause, if any.

Copy link
Member Author

Choose a reason for hiding this comment

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

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't know why the official suggestion isn't typing.ContextManager - maybe b/c it was only introduced in 3.6.

Copy link
Member

Choose a reason for hiding this comment

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

Yea strange, seems like that's what is returned from .as_file and such: https://github.com/python/typeshed/blob/cff2b3db0ca291e153a8bd407f48ac220a381dd8/stdlib/importlib/resources/__init__.pyi#L37
It is returning an AbstractContextManager vs. ContextManager, but seems like those two match anyway (AbstractContextManager, ContextManager)

return ExpressJsPlugin(
package_json["name"],
package_json["version"],
Copy link
Member

Choose a reason for hiding this comment

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

Did we discuss getting the version from python instead of from the package.json? So that package.json has a dummy version, and we always pull from the python version so they always match

Copy link
Member Author

Choose a reason for hiding this comment

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

I think this is for @jnumainville to discuss and move forward if desired. Also, questions around editable installs and potentially re-pointing the Path for development use-cases.

package_json["main"],
root_provider,
)


def _production_root() -> typing.Generator[pathlib.Path, None, None]:
# TODO: Js content should be in same package directory
# https://github.com/deephaven/deephaven-plugins/issues/139
if sys.version_info < (3, 9):
return importlib.resources.path("js", "plotly-express")
else:
return importlib.resources.as_file(
importlib.resources.files("js").joinpath("plotly-express")
)


def _development_root() -> typing.Generator[pathlib.Path, None, None]:
raise NotImplementedError("TODO")


def create_js_plugin() -> JsPlugin:
# TODO: Include developer instructions for installing in editable mode
# https://github.com/deephaven/deephaven-plugins/issues/93
# TBD what editable mode looks like for JsPlugin
return _create_from_npm_package_json(_production_root)