From afecb68b556b96e8262966208e39bb69ad9bf964 Mon Sep 17 00:00:00 2001 From: William Chargin Date: Mon, 24 Jun 2019 16:24:56 -0700 Subject: [PATCH] plugins: add sample dynamically loaded plugin (#2354) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: This plugin can eventually replace the `tensorboard-plugin-example` repository; it’ll be easier for us to keep it up to date if it’s in the same repository as main TensorBoard. And it has tests! Test Plan: Run `bazel test //tensorboard/plugins/example:smoke_test` to start. Then, modify `setup.py` to remove the `entry_points` entry, and note that the test fails. Separately, modify line 42 of `__init__.py` to serve the JS under `/not_index.js` instead; the test should again fail. Test the development workflow: Follow the instructions in `README.md`. Then, edit `widgets.js` (say, add another exclamation point) and refresh to see changes live without restarting TensorBoard: ![screenshot of “Hello TensorBoard!” in example plugin][1] [1]: https://user-images.githubusercontent.com/4317806/59641012-cb700380-9114-11e9-8323-c811388df31c.png wchargin-branch: example-dynamic-plugin --- tensorboard/plugins/example/.gitignore | 21 +++++++ tensorboard/plugins/example/BUILD | 25 ++++++++ tensorboard/plugins/example/README.md | 48 +++++++++++++++ tensorboard/plugins/example/setup.py | 36 +++++++++++ tensorboard/plugins/example/smoke_test.sh | 61 +++++++++++++++++++ .../tensorboard_plugin_example/__init__.py | 60 ++++++++++++++++++ .../static/index.js | 20 ++++++ 7 files changed, 271 insertions(+) create mode 100644 tensorboard/plugins/example/.gitignore create mode 100644 tensorboard/plugins/example/BUILD create mode 100644 tensorboard/plugins/example/README.md create mode 100644 tensorboard/plugins/example/setup.py create mode 100755 tensorboard/plugins/example/smoke_test.sh create mode 100644 tensorboard/plugins/example/tensorboard_plugin_example/__init__.py create mode 100644 tensorboard/plugins/example/tensorboard_plugin_example/static/index.js diff --git a/tensorboard/plugins/example/.gitignore b/tensorboard/plugins/example/.gitignore new file mode 100644 index 00000000000..4fb00b5abe5 --- /dev/null +++ b/tensorboard/plugins/example/.gitignore @@ -0,0 +1,21 @@ +# Copyright 2019 The TensorFlow Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +# Generated by running `python setup.py develop`. +tensorboard_plugin_example.egg-info/ + +# Generated by running `python setup.py bdist_wheel`. +build/ +dist/ diff --git a/tensorboard/plugins/example/BUILD b/tensorboard/plugins/example/BUILD new file mode 100644 index 00000000000..20e99a9c01a --- /dev/null +++ b/tensorboard/plugins/example/BUILD @@ -0,0 +1,25 @@ +# Description: +# TensorBoard example plugin + +# This plugin is not primarily built with Bazel (intentionally, to +# demonstrate that Bazel is not required); we use this BUILD file only +# to define integration tests for TensorBoard itself. + +package(default_visibility = ["//tensorboard:internal"]) + +licenses(["notice"]) # Apache 2.0 + +sh_test( + name = "smoke_test", + size = "large", + timeout = "short", + srcs = ["smoke_test.sh"], + # Don't just `glob(["**"])` because `setup.py` creates artifacts + # like wheels and a `tensorboard_plugin_example.egg-info` directory, + # and we want to make sure that those don't interfere with the test. + data = [ + "setup.py", + "tensorboard_plugin_example/__init__.py", + "//tensorboard/pip_package", + ] + glob(["tensorboard_plugin_example/static/**"]), +) diff --git a/tensorboard/plugins/example/README.md b/tensorboard/plugins/example/README.md new file mode 100644 index 00000000000..de5b949fbbc --- /dev/null +++ b/tensorboard/plugins/example/README.md @@ -0,0 +1,48 @@ +# Example plugin + +## Overview + +A sample plugin using TensorBoard’s dynamic plugin loading mechanism. It +doesn’t do anything useful at the moment, but a stock TensorBoard binary +(1.14+) will automatically load and display it. + +This code happens to live in the TensorBoard repository, but is intended +to stand in for a third-party plugin developer’s separate repository. It +is not built with Bazel, and it only depends on TensorBoard through its +public APIs (under the assumption that TensorBoard is installed in an +active virtualenv). + +## Usage + +In this directory (`tensorboard/plugins/example`), in a virtualenv with +TensorBoard installed, run: + +``` +python setup.py develop +``` + +This will link the plugin into your virtualenv. You can edit the +plugin’s source code and data files and see changes reflected without +having to reinstall the plugin. + +Then, just run + +``` +tensorboard --logdir /tmp/whatever +``` + +and open TensorBoard to see the plugin’s “hello world” screen. + +After making changes to the Python code, you must restart TensorBoard +for your changes to take effect. If your plugin reads data files at +runtime, you can edit those and see changes reflected immediately. + +You can run + +``` +python setup.py develop --uninstall +``` + +to unlink the plugin from your virtualenv, after which you can also +delete the `tensorboard_plugin_example.egg-info/` directory that the +original `setup.py` invocation created. diff --git a/tensorboard/plugins/example/setup.py b/tensorboard/plugins/example/setup.py new file mode 100644 index 00000000000..772ce62f6d2 --- /dev/null +++ b/tensorboard/plugins/example/setup.py @@ -0,0 +1,36 @@ +# Copyright 2019 The TensorFlow Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import setuptools + + +setuptools.setup( + name="tensorboard_plugin_example", + version="0.1.0", + description="Sample TensorBoard plugin.", + packages=["tensorboard_plugin_example"], + package_data={ + "tensorboard_plugin_example": ["static/**"], + }, + entry_points={ + "tensorboard_plugins": [ + "example = tensorboard_plugin_example:ExamplePlugin", + ], + }, +) diff --git a/tensorboard/plugins/example/smoke_test.sh b/tensorboard/plugins/example/smoke_test.sh new file mode 100755 index 00000000000..0bb06687fe9 --- /dev/null +++ b/tensorboard/plugins/example/smoke_test.sh @@ -0,0 +1,61 @@ +#!/bin/sh +# Copyright 2019 The TensorFlow Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -eux +command -v virtualenv >/dev/null + +workdir="$(mktemp -d)" +cd "${workdir}" + +cleanup() { + rm -r "${workdir}" +} +trap cleanup EXIT + +cp -LR "${TEST_SRCDIR}/org_tensorflow_tensorboard/tensorboard/plugins/example/" \ + ./example-plugin/ + +mkdir tensorboard-wheels +tar xzvf \ + "${TEST_SRCDIR}/org_tensorflow_tensorboard/tensorboard/pip_package/pip_packages.tar.gz" \ + -C ./tensorboard-wheels/ + +virtualenv venv +export VIRTUAL_ENV=venv +export PATH="${PWD}/venv/bin:${PATH}" +unset PYTHON_HOME + +# Require wheel for bdist_wheel command, and setuptools 36.2.0+ so that +# env markers are handled (https://github.com/pypa/setuptools/pull/1081) +pip install -qU wheel 'setuptools>=36.2.0' + +(cd ./example-plugin && python setup.py bdist_wheel) +[ -f ./example-plugin/dist/*.whl ] # just one wheel + +# This plugin doesn't require TensorFlow, so we don't install it. +py_major_version="$(python -c 'import sys; print(sys.version_info[0])')" +pip install ./tensorboard-wheels/*py"${py_major_version}"*.whl +pip install ./example-plugin/dist/*.whl + +# Test tensorboard + tensorboard_plugin_example integration. +mkfifo pipe +tensorboard --port=0 --logdir=smokedir 2>pipe & +perl -ne 'print STDERR;/http:.*:(\d+)/ and print $1.v10 and exit 0' port +curl -fs "http://localhost:$(cat port)/data/plugins_listing" >plugins_listing +grep -F '"example":' plugins_listing +grep -F '"/data/plugin/example/index.js"' plugins_listing +curl -fs "http://localhost:$(cat port)/data/plugin/example/index.js" >index.js +diff -u example-plugin/tensorboard_plugin_example/static/index.js index.js +kill $! diff --git a/tensorboard/plugins/example/tensorboard_plugin_example/__init__.py b/tensorboard/plugins/example/tensorboard_plugin_example/__init__.py new file mode 100644 index 00000000000..0c7a37b8202 --- /dev/null +++ b/tensorboard/plugins/example/tensorboard_plugin_example/__init__.py @@ -0,0 +1,60 @@ +# Copyright 2019 The TensorFlow Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""A sample plugin to demonstrate dynamic loading.""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import os + +from tensorboard.plugins import base_plugin +import werkzeug +from werkzeug import wrappers + + +class ExamplePlugin(base_plugin.TBPlugin): + plugin_name = 'example' + + def __init__(self, context): + # A real plugin would likely save the `context.multiplexer` and/or + # `context.db_connection_provider` attributes for later use, but we + # don't actually need any of that. + pass + + def is_active(self): + return True + + def get_plugin_apps(self): + return { + "/index.js": self._serve_js, + } + + def frontend_metadata(self): + return super(ExamplePlugin, self).frontend_metadata()._replace( + es_module_path="/index.js", + ) + + @wrappers.Request.application + def _serve_js(self, request): + del request # unused + filepath = os.path.join(os.path.dirname(__file__), "static", "index.js") + with open(filepath) as infile: + contents = infile.read() + return werkzeug.Response( + contents, + status=200, + content_type="application/javascript", + ) diff --git a/tensorboard/plugins/example/tensorboard_plugin_example/static/index.js b/tensorboard/plugins/example/tensorboard_plugin_example/static/index.js new file mode 100644 index 00000000000..7ab054fe603 --- /dev/null +++ b/tensorboard/plugins/example/tensorboard_plugin_example/static/index.js @@ -0,0 +1,20 @@ +// Copyright 2019 The TensorFlow Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ============================================================================== + +export function render() { + const msg = document.createElement("span"); + msg.innerText = "Hello TensorBoard!"; + document.body.appendChild(msg); +}