\d+))?
7 | serialize =
8 | {major}.{minor}.{patch}{release}{build}
9 | {major}.{minor}.{patch}
10 |
11 | [bumpversion:part:release]
12 | optional_value = g
13 | first_value = g
14 | values =
15 | a
16 | b
17 | g
18 |
19 | [bumpversion:file:ipyvue/_version.py]
20 |
21 | [bumpversion:file:js/package.json]
22 |
--------------------------------------------------------------------------------
/js/src/ForceLoad.js:
--------------------------------------------------------------------------------
1 | /* eslint camelcase: off */
2 | import { DOMWidgetModel } from '@jupyter-widgets/base';
3 |
4 | export class ForceLoadModel extends DOMWidgetModel {
5 | defaults() {
6 | return {
7 | ...super.defaults(),
8 | ...{
9 | _model_name: 'ForceLoadModel',
10 | _model_module: 'jupyter-vue',
11 | _model_module_version: '^0.0.1',
12 | },
13 | };
14 | }
15 | }
16 |
17 | ForceLoadModel.serializers = {
18 | ...DOMWidgetModel.serializers,
19 | };
20 |
--------------------------------------------------------------------------------
/js/src/nodeps.js:
--------------------------------------------------------------------------------
1 | export { default as Vue } from 'vue';
2 | export { VueModel } from './VueModel';
3 | export { VueTemplateModel } from './VueTemplateModel';
4 | export { VueView, createViewContext } from './VueView';
5 | export { HtmlModel } from './Html';
6 | export { TemplateModel } from './Template';
7 | export { ForceLoadModel } from './ForceLoad';
8 | export { vueRender } from './VueRenderer';
9 | export { VueComponentModel } from './VueComponentModel';
10 |
11 | export const { version } = require('../package.json'); // eslint-disable-line global-require
12 |
--------------------------------------------------------------------------------
/js/src/index.js:
--------------------------------------------------------------------------------
1 | export { default as Vue } from './VueWithCompiler';
2 | export { VueModel } from './VueModel';
3 | export { VueTemplateModel } from './VueTemplateModel';
4 | export { VueView, createViewContext } from './VueView';
5 | export { HtmlModel } from './Html';
6 | export { TemplateModel } from './Template';
7 | export { ForceLoadModel } from './ForceLoad';
8 | export { vueRender } from './VueRenderer';
9 | export { VueComponentModel } from './VueComponentModel';
10 |
11 | export const { version } = require('../package.json'); // eslint-disable-line global-require
12 |
--------------------------------------------------------------------------------
/js/src/extension.js:
--------------------------------------------------------------------------------
1 | // This file contains the javascript that is run when the notebook is loaded.
2 | // It contains some requirejs configuration and the `load_ipython_extension`
3 | // which is required for any notebook extension.
4 |
5 | // Configure requirejs
6 | if (window.require) {
7 | window.require.config({
8 | map: {
9 | '*': {
10 | 'jupyter-vue': 'nbextensions/jupyter-vue/index',
11 | },
12 | },
13 | });
14 | }
15 |
16 | // Export the required load_ipython_extension
17 | module.exports = {
18 | load_ipython_extension() {},
19 | };
20 |
--------------------------------------------------------------------------------
/RELEASE.md:
--------------------------------------------------------------------------------
1 | # Release a new version of ipyvue on PyPI and NPM:
2 |
3 | - assert you have a remote called "upstream" pointing to git@github.com:widgetti/ipyvue.git
4 | - assert you have an up-to-date and clean working directory on the branch master
5 | - run `./release.sh [patch | minor | major]`
6 |
7 | ## Making an alpha release
8 |
9 | $ ./release.sh patch --new-version 1.19.0a1
10 |
11 | ## Recover from a failed release
12 |
13 | If a release fails on CI, and you want to keep the history clean
14 | ```
15 | # do fix
16 | $ git rebase -i HEAD~3
17 | $ git tag v1.19.0 -f && git push upstream master v1.19.0 -f
18 | ```
19 |
--------------------------------------------------------------------------------
/js/src/Html.js:
--------------------------------------------------------------------------------
1 | import { VueModel } from './VueModel';
2 |
3 | export
4 | class HtmlModel extends VueModel {
5 | defaults() {
6 | return {
7 | ...super.defaults(),
8 | ...{
9 | _model_name: 'HtmlModel',
10 | tag: null,
11 | },
12 | };
13 | }
14 |
15 | getVueTag() { // eslint-disable-line class-methods-use-this
16 | if (this.get('tag').toLowerCase().includes('script')) {
17 | return undefined;
18 | }
19 | return this.get('tag');
20 | }
21 | }
22 |
23 | HtmlModel.serializers = {
24 | ...VueModel.serializers,
25 | };
26 |
--------------------------------------------------------------------------------
/js/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | browser: true,
4 | es6: true,
5 | },
6 | extends: 'airbnb-base',
7 | globals: {
8 | Atomics: 'readonly',
9 | SharedArrayBuffer: 'readonly',
10 | },
11 | parserOptions: {
12 | ecmaVersion: 2018,
13 | sourceType: 'module',
14 | },
15 | plugins: [
16 | 'vue',
17 | ],
18 | rules: {
19 | 'indent': ['error', 4, { 'SwitchCase': 1 }],
20 | 'import/prefer-default-export': 'off',
21 | 'camelcase': ["error", {allow: ['__webpack_public_path__', 'load_ipython_extension', '_model_name']}],
22 | 'no-underscore-dangle': 'off',
23 | 'class-methods-use-this': 'off',
24 | 'no-use-before-define': 'off'
25 | },
26 | };
27 |
--------------------------------------------------------------------------------
/ipyvue/__init__.py:
--------------------------------------------------------------------------------
1 | from ._version import __version__
2 | from .Html import Html
3 | from .Template import Template, watch
4 | from .VueWidget import VueWidget
5 | from .VueTemplateWidget import VueTemplate
6 | from .VueComponentRegistry import (
7 | VueComponent,
8 | register_component_from_string,
9 | register_component_from_file,
10 | )
11 |
12 |
13 | def _jupyter_labextension_paths():
14 | return [
15 | {
16 | "src": "labextension",
17 | "dest": "jupyter-vue",
18 | }
19 | ]
20 |
21 |
22 | def _jupyter_nbextension_paths():
23 | return [
24 | {
25 | "section": "notebook",
26 | "src": "nbextension",
27 | "dest": "jupyter-vue",
28 | "require": "jupyter-vue/extension",
29 | }
30 | ]
31 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Mario Buikhuizen
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/js/src/VueTemplateModel.js:
--------------------------------------------------------------------------------
1 | /* eslint camelcase: off */
2 | import { DOMWidgetModel, unpack_models } from '@jupyter-widgets/base';
3 |
4 | export class VueTemplateModel extends DOMWidgetModel {
5 | defaults() {
6 | return {
7 | ...super.defaults(),
8 | ...{
9 | _jupyter_vue: null,
10 | _model_name: 'VueTemplateModel',
11 | _view_name: 'VueView',
12 | _view_module: 'jupyter-vue',
13 | _model_module: 'jupyter-vue',
14 | _view_module_version: '^0.0.3',
15 | _model_module_version: '^0.0.3',
16 | template: null,
17 | css: null,
18 | methods: null,
19 | data: null,
20 | events: null,
21 | _component_instances: null,
22 | },
23 | };
24 | }
25 | }
26 |
27 | VueTemplateModel.serializers = {
28 | ...DOMWidgetModel.serializers,
29 | template: { deserialize: unpack_models },
30 | components: { deserialize: unpack_models },
31 | _component_instances: { deserialize: unpack_models },
32 | };
33 |
--------------------------------------------------------------------------------
/resources/msd-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/js/src/VueView.js:
--------------------------------------------------------------------------------
1 | import { DOMWidgetView } from '@jupyter-widgets/base';
2 | import Vue from 'vue';
3 | import { vueRender } from './VueRenderer';
4 |
5 | export function createViewContext(view) {
6 | return {
7 | getModelById(modelId) {
8 | return view.model.widget_manager.get_model(modelId);
9 | },
10 | /* TODO: refactor to abstract the direct use of WidgetView away */
11 | getView() {
12 | return view;
13 | },
14 | };
15 | }
16 |
17 | export class VueView extends DOMWidgetView {
18 | remove() {
19 | this.vueApp.$destroy();
20 | return super.remove();
21 | }
22 |
23 | render() {
24 | super.render();
25 | this.displayed.then(() => {
26 | const vueEl = document.createElement('div');
27 | this.el.appendChild(vueEl);
28 |
29 | this.vueApp = new Vue({
30 | el: vueEl,
31 | provide: {
32 | viewCtx: createViewContext(this),
33 | },
34 | render: createElement => vueRender(createElement, this.model, this, {}),
35 | });
36 | });
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/js/src/VueModel.js:
--------------------------------------------------------------------------------
1 | /* eslint camelcase: off */
2 | import {
3 | DOMWidgetModel, unpack_models,
4 | } from '@jupyter-widgets/base';
5 |
6 | export class VueModel extends DOMWidgetModel {
7 | defaults() {
8 | return {
9 | ...super.defaults(),
10 | ...{
11 | _jupyter_vue: null,
12 | _model_name: 'VueModel',
13 | _view_name: 'VueView',
14 | _view_module: 'jupyter-vue',
15 | _model_module: 'jupyter-vue',
16 | _view_module_version: '^0.0.3',
17 | _model_module_version: '^0.0.3',
18 | _metadata: null,
19 | children: undefined,
20 | slot: null,
21 | _events: null,
22 | v_model: '!!disabled!!',
23 | style_: null,
24 | class_: null,
25 | attributes: null,
26 | v_slots: null,
27 | v_on: null,
28 | },
29 | };
30 | }
31 | }
32 |
33 | VueModel.serializers = {
34 | ...DOMWidgetModel.serializers,
35 | children: { deserialize: unpack_models },
36 | v_slots: { deserialize: unpack_models },
37 | };
38 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://www.npmjs.com/package/jupyter-vue)
2 | [](https://pypi.python.org/project/ipyvue)
3 | [](https://anaconda.org/conda-forge/ipyvue)
4 | [](https://github.com/psf/black)
5 |
6 | ipyvue
7 | ======
8 |
9 | Jupyter widgets base for [Vue](https://vuejs.org/) libraries
10 |
11 | Installation
12 | ------------
13 |
14 | To install use pip:
15 |
16 | $ pip install ipyvue
17 | $ jupyter nbextension enable --py --sys-prefix ipyvue
18 |
19 |
20 | For a development installation (requires npm),
21 |
22 | $ git clone https://github.com/mariobuikhuizen/ipyvue.git
23 | $ cd ipyvue
24 | $ pip install -e ".[dev]"
25 | $ jupyter nbextension install --py --symlink --sys-prefix ipyvue
26 | $ jupyter nbextension enable --py --sys-prefix ipyvue
27 | $ jupyter labextension develop . --overwrite
28 |
29 | Sponsors
30 | --------
31 |
32 | Project ipyvue receives direct funding from the following sources:
33 |
34 | [](https://msd.com)
35 |
--------------------------------------------------------------------------------
/ipyvue/VueComponentRegistry.py:
--------------------------------------------------------------------------------
1 | import os
2 | from traitlets import Unicode
3 | from ipywidgets import DOMWidget
4 | from ._version import semver
5 |
6 |
7 | class VueComponent(DOMWidget):
8 | _model_name = Unicode("VueComponentModel").tag(sync=True)
9 | _model_module = Unicode("jupyter-vue").tag(sync=True)
10 | _model_module_version = Unicode(semver).tag(sync=True)
11 |
12 | name = Unicode().tag(sync=True)
13 | component = Unicode().tag(sync=True)
14 |
15 |
16 | vue_component_registry = {}
17 | vue_component_files = {}
18 |
19 |
20 | def register_component_from_string(name, value):
21 | components = vue_component_registry
22 |
23 | if name in components.keys():
24 | comp = components[name]
25 | comp.component = value
26 | else:
27 | comp = VueComponent(name=name, component=value)
28 | components[name] = comp
29 |
30 |
31 | def register_component_from_file(name, file_name, relative_to_file=None):
32 | # for backward compatibility with previous argument arrangement
33 | if name is None:
34 | name = file_name
35 | file_name = relative_to_file
36 | relative_to_file = None
37 |
38 | if relative_to_file:
39 | file_name = os.path.join(os.path.dirname(relative_to_file), file_name)
40 | with open(file_name) as f:
41 | vue_component_files[os.path.abspath(file_name)] = name
42 | register_component_from_string(name, f.read())
43 |
44 |
45 | __all__ = [
46 | "VueComponent",
47 | "register_component_from_string",
48 | "register_component_from_file",
49 | ]
50 |
--------------------------------------------------------------------------------
/examples/hot-reload/hot-reload.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "code",
5 | "execution_count": null,
6 | "id": "08b3d099",
7 | "metadata": {},
8 | "outputs": [],
9 | "source": [
10 | "import ipyvue\n",
11 | "from mywidget import MyWidget\n",
12 | "\n",
13 | "# Uncomment the next line to install the watchdog package\n",
14 | "# needed for the watch function\n",
15 | "\n",
16 | "# !pip install watchdog\n",
17 | "\n",
18 | "ipyvue.watch('.')"
19 | ]
20 | },
21 | {
22 | "cell_type": "code",
23 | "execution_count": null,
24 | "id": "bac5035f",
25 | "metadata": {},
26 | "outputs": [],
27 | "source": [
28 | "mywidget = MyWidget(some_data={\n",
29 | " 'message': 'Hello',\n",
30 | " 'items': ['item a', 'item b', 'item c']\n",
31 | "})\n",
32 | "mywidget"
33 | ]
34 | },
35 | {
36 | "cell_type": "code",
37 | "execution_count": null,
38 | "id": "1a258159",
39 | "metadata": {},
40 | "outputs": [],
41 | "source": [
42 | "# Now edit one of the .vue files and see the changes directly without a kernel reload "
43 | ]
44 | }
45 | ],
46 | "metadata": {
47 | "kernelspec": {
48 | "display_name": "Python 3 (ipykernel)",
49 | "language": "python",
50 | "name": "python3"
51 | },
52 | "language_info": {
53 | "codemirror_mode": {
54 | "name": "ipython",
55 | "version": 3
56 | },
57 | "file_extension": ".py",
58 | "mimetype": "text/x-python",
59 | "name": "python",
60 | "nbconvert_exporter": "python",
61 | "pygments_lexer": "ipython3",
62 | "version": "3.9.6"
63 | }
64 | },
65 | "nbformat": 4,
66 | "nbformat_minor": 5
67 | }
68 |
--------------------------------------------------------------------------------
/tests/ui/test_events.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | import sys
3 |
4 | if sys.version_info < (3, 7):
5 | pytest.skip("requires python3.7 or higher", allow_module_level=True)
6 | import ipyvue as vue
7 | import playwright.sync_api
8 | from IPython.display import display
9 | from unittest.mock import MagicMock
10 |
11 |
12 | def test_event_basics(solara_test, page_session: playwright.sync_api.Page):
13 | inner = vue.Html(tag="div", children=["Click Me!"])
14 | outer = vue.Html(tag="dev", children=[inner])
15 | mock_outer = MagicMock()
16 |
17 | def on_click_inner(*ignore):
18 | inner.children = ["Clicked"]
19 |
20 | # should stop propagation
21 | inner.on_event("click.stop", on_click_inner)
22 | outer.on_event("click", mock_outer)
23 |
24 | display(outer)
25 | inner_sel = page_session.locator("text=Click Me!")
26 | inner_sel.wait_for()
27 | inner_sel.click()
28 | page_session.locator("text=Clicked").wait_for()
29 | mock_outer.assert_not_called()
30 |
31 | # reset
32 | inner.children = ["Click Me!"]
33 | # Now we should NOT stop propagation
34 | inner.on_event("click", on_click_inner)
35 | inner_sel.wait_for()
36 | inner_sel.click()
37 | page_session.locator("text=Clicked").wait_for()
38 | mock_outer.assert_called_once()
39 |
40 |
41 | def test_mouse_event(solara_test, page_session: playwright.sync_api.Page):
42 | div = vue.Html(tag="div", children=["Click Me!"])
43 | last_event_data = None
44 |
45 | def on_click(widget, event, data):
46 | nonlocal last_event_data
47 | last_event_data = data
48 | div.children = ["Clicked"]
49 |
50 | div.on_event("click", on_click)
51 | display(div)
52 |
53 | # click in the div
54 | box = page_session.locator("text=Click Me!").bounding_box()
55 | assert box is not None
56 | page_session.mouse.click(box["x"], box["y"])
57 |
58 | page_session.locator("text=Clicked").wait_for()
59 | assert last_event_data is not None
60 | assert last_event_data["x"] == box["x"]
61 | assert last_event_data["y"] == box["y"]
62 |
--------------------------------------------------------------------------------
/js/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "jupyter-vue",
3 | "version": "1.11.3",
4 | "description": "Jupyter widgets base for Vue libraries",
5 | "license": "MIT",
6 | "author": "Mario Buikhuizen, Maarten Breddels",
7 | "main": "lib/index.js",
8 | "repository": {
9 | "type": "git",
10 | "url": "https://github.com/mariobuikhuizen/ipyvue.git"
11 | },
12 | "keywords": [
13 | "jupyter",
14 | "widgets",
15 | "ipython",
16 | "ipywidgets",
17 | "jupyterlab-extension"
18 | ],
19 | "files": [
20 | "src/",
21 | "lib/",
22 | "dist/"
23 | ],
24 | "browserslist": ">0.8%, not ie 11, not op_mini all, not dead",
25 | "scripts": {
26 | "build:babel": "babel src --out-dir lib --copy-files",
27 | "watch:babel": "babel src --watch --out-dir lib --copy-files --verbose",
28 | "build:labextension": "jupyter labextension build .",
29 | "watch:labextension": "jupyter labextension watch .",
30 | "build:webpack": "webpack",
31 | "watch:webpack": "webpack --mode development --watch",
32 | "watch": "run-p watch:*",
33 | "clean": "rimraf lib/ dist/",
34 | "prepare": "run-s build:*",
35 | "test": "echo \"Error: no test specified\" && exit 1"
36 | },
37 | "devDependencies": {
38 | "@babel/cli": "^7.5.0",
39 | "@babel/core": "^7.4.4",
40 | "@babel/preset-env": "^7.4.4",
41 | "@jupyterlab/builder": "^3",
42 | "ajv": "^6.10.0",
43 | "css-loader": "^5",
44 | "eslint": "^5.16.0",
45 | "eslint-config-airbnb-base": "^13.1.0",
46 | "eslint-plugin-import": "^2.17.2",
47 | "eslint-plugin-vue": "^5.2.2",
48 | "file-loader": "^6",
49 | "npm-run-all": "^4.1.5",
50 | "rimraf": "^2.6.3",
51 | "style-loader": "^0.23.1",
52 | "webpack": "^5",
53 | "webpack-cli": "^4"
54 | },
55 | "dependencies": {
56 | "@jupyter-widgets/base": "^1 || ^2 || ^3 || ^4",
57 | "@mariobuikhuizen/vue-compiler-addon": "^2.6.10-alpha.2",
58 | "core-js": "^3.0.1",
59 | "lodash": "^4.17.11",
60 | "uuid": "^3.4.0",
61 | "vue": "^2.6.10"
62 | },
63 | "jupyterlab": {
64 | "extension": "lib/labplugin",
65 | "outputDir": "../ipyvue/labextension",
66 | "sharedPackages": {
67 | "@jupyter-widgets/base": {
68 | "bundled": false,
69 | "singleton": true
70 | }
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/ipyvue/Template.py:
--------------------------------------------------------------------------------
1 | import os
2 | from traitlets import Unicode
3 | from ipywidgets import Widget
4 |
5 | from .VueComponentRegistry import vue_component_files, register_component_from_file
6 | from ._version import semver
7 |
8 | template_registry = {}
9 |
10 |
11 | def watch(paths=""):
12 | import logging
13 | from watchdog.observers import Observer
14 | from watchdog.events import FileSystemEventHandler
15 |
16 | log = logging.getLogger("ipyvue")
17 |
18 | class VueEventHandler(FileSystemEventHandler):
19 | def on_modified(self, event):
20 | super(VueEventHandler, self).on_modified(event)
21 | if not event.is_directory:
22 | if event.src_path in template_registry:
23 | log.info(f"updating: {event.src_path}")
24 | with open(event.src_path) as f:
25 | template_registry[event.src_path].template = f.read()
26 | elif event.src_path in vue_component_files:
27 | log.info(f"updating component: {event.src_path}")
28 | name = vue_component_files[event.src_path]
29 | register_component_from_file(name, event.src_path)
30 |
31 | observer = Observer()
32 |
33 | if not isinstance(paths, (list, tuple)):
34 | paths = [paths]
35 |
36 | for path in paths:
37 | path = os.path.normpath(path)
38 | log.info(f"watching {path}")
39 | observer.schedule(VueEventHandler(), path, recursive=True)
40 |
41 | observer.start()
42 |
43 |
44 | def get_template(abs_path):
45 | abs_path = os.path.normpath(abs_path)
46 | if abs_path not in template_registry:
47 | with open(abs_path, encoding="utf-8") as f:
48 | tw = Template(template=f.read())
49 | template_registry[abs_path] = tw
50 | else:
51 | with open(abs_path, encoding="utf-8") as f:
52 | template_registry[abs_path].template = f.read()
53 | return template_registry[abs_path]
54 |
55 |
56 | class Template(Widget):
57 | _model_name = Unicode("TemplateModel").tag(sync=True)
58 | _model_module = Unicode("jupyter-vue").tag(sync=True)
59 | _model_module_version = Unicode(semver).tag(sync=True)
60 |
61 | template = Unicode(None, allow_none=True).tag(sync=True)
62 |
63 |
64 | __all__ = ["Template", "watch"]
65 |
--------------------------------------------------------------------------------
/examples/DeepWatch.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "code",
5 | "execution_count": null,
6 | "metadata": {},
7 | "outputs": [],
8 | "source": [
9 | "import ipyvuetify as v\n",
10 | "import traitlets\n",
11 | "class MyDeep(v.VuetifyTemplate):\n",
12 | " deep = traitlets.Dict({'prop': 1,\n",
13 | " 'deeper': {'prop': 2}}).tag(sync=True)\n",
14 | " not_deep = traitlets.Unicode('x').tag(sync=True)\n",
15 | " deep_array = traitlets.List(['array']).tag(sync=True)\n",
16 | " deep_array2 = traitlets.List([{'prop': 'array2'}]).tag(sync=True)\n",
17 | " template = traitlets.Unicode('''\n",
18 | " \n",
19 | " \n",
20 | " \n",
21 | " \n",
22 | " \n",
23 | " \n",
24 | " {{ deep.prop }} |\n",
25 | " {{ deep.deeper.prop }} |\n",
26 | " {{ not_deep }} |\n",
27 | " {{ deep_array[0] }} |\n",
28 | " {{ deep_array2[0].prop }}\n",
29 | "
\n",
30 | " ''').tag(sync=True)\n",
31 | " \n",
32 | "md = MyDeep()\n",
33 | "md"
34 | ]
35 | },
36 | {
37 | "cell_type": "code",
38 | "execution_count": null,
39 | "metadata": {},
40 | "outputs": [],
41 | "source": [
42 | "[md.deep, md.not_deep, md.deep_array, md.deep_array2]"
43 | ]
44 | },
45 | {
46 | "cell_type": "code",
47 | "execution_count": null,
48 | "metadata": {},
49 | "outputs": [],
50 | "source": [
51 | "md.deep['deeper']"
52 | ]
53 | },
54 | {
55 | "cell_type": "code",
56 | "execution_count": null,
57 | "metadata": {},
58 | "outputs": [],
59 | "source": []
60 | }
61 | ],
62 | "metadata": {
63 | "kernelspec": {
64 | "display_name": "Python 3",
65 | "language": "python",
66 | "name": "python3"
67 | },
68 | "language_info": {
69 | "codemirror_mode": {
70 | "name": "ipython",
71 | "version": 3
72 | },
73 | "file_extension": ".py",
74 | "mimetype": "text/x-python",
75 | "name": "python",
76 | "nbconvert_exporter": "python",
77 | "pygments_lexer": "ipython3",
78 | "version": "3.7.6"
79 | }
80 | },
81 | "nbformat": 4,
82 | "nbformat_minor": 4
83 | }
84 |
--------------------------------------------------------------------------------
/js/src/VueComponentModel.js:
--------------------------------------------------------------------------------
1 | /* eslint camelcase: off */
2 | import { DOMWidgetModel } from '@jupyter-widgets/base';
3 | import Vue from 'vue';
4 | import httpVueLoader from './httpVueLoader';
5 | import {TemplateModel} from './Template';
6 |
7 | export class VueComponentModel extends DOMWidgetModel {
8 | defaults() {
9 | return {
10 | ...super.defaults(),
11 | ...{
12 | _model_name: 'VueComponentModel',
13 | _model_module: 'jupyter-vue',
14 | _model_module_version: '^0.0.3',
15 | name: null,
16 | component: null,
17 | },
18 | };
19 | }
20 |
21 | constructor(...args) {
22 | super(...args);
23 |
24 | const [, { widget_manager }] = args;
25 |
26 | const name = this.get('name');
27 | Vue.component(name, httpVueLoader(this.get('component')));
28 | this.on('change:component', () => {
29 | Vue.component(name, httpVueLoader(this.get('component')));
30 |
31 | (async () => {
32 | const models = await Promise.all(Object.values(widget_manager._models));
33 | const componentModels = models
34 | .filter(model => model instanceof VueComponentModel);
35 |
36 | const affectedComponents = [];
37 |
38 | function re(searchName) {
39 | return new RegExp(`\\<${searchName}[ />\n]`, 'g');
40 | }
41 |
42 | function find_usage(searchName) {
43 | affectedComponents.push(searchName);
44 | componentModels
45 | .filter(model => model.get('component').match(re(searchName)))
46 | .forEach((model) => {
47 | const cname = model.get('name');
48 | if (!affectedComponents.includes(cname)) {
49 | find_usage(cname);
50 | }
51 | });
52 | }
53 |
54 | find_usage(name);
55 |
56 | const affectedTemplateModels = models
57 | .filter(model => model instanceof TemplateModel
58 | && affectedComponents.some(cname => model.get('template').match(re(cname))));
59 |
60 | affectedTemplateModels.forEach(model => model.trigger('change:template'));
61 | })();
62 | });
63 | }
64 | }
65 |
66 | VueComponentModel.serializers = {
67 | ...DOMWidgetModel.serializers,
68 | };
69 |
--------------------------------------------------------------------------------
/tests/ui/test_watchers.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | import sys
3 | from playwright.sync_api import Page
4 | from traitlets import Callable, Int, Unicode, default
5 | from IPython.display import display
6 |
7 | if sys.version_info < (3, 7):
8 | pytest.skip("requires python3.7 or higher", allow_module_level=True)
9 |
10 | import ipyvue as vue
11 |
12 |
13 | class WatcherTemplateTraitlet(vue.VueTemplate):
14 | number = Int(0).tag(sync=True)
15 | callback = Callable()
16 | text = Unicode("Click Me ").tag(sync=True)
17 |
18 | @default("template")
19 | def _default_vue_template(self):
20 | return """
21 |
22 | {{text + number}}
23 |
24 |
33 | """
34 |
35 | def vue_callback(self):
36 | self.callback()
37 |
38 |
39 | # We test that the watcher is activated when a var from python is changed
40 | def test_watcher_traitlet(solara_test, page_session: Page):
41 | def callback():
42 | widget.text = "Clicked "
43 |
44 | widget = WatcherTemplateTraitlet(callback=callback)
45 |
46 | display(widget)
47 |
48 | widget = page_session.locator("text=Click Me 0")
49 | widget.click()
50 | widget = page_session.locator("text=Clicked 1")
51 |
52 |
53 | class WatcherTemplateVue(vue.VueTemplate):
54 | callback = Callable()
55 | text = Unicode("Click Me ").tag(sync=True)
56 |
57 | @default("template")
58 | def _default_vue_template(self):
59 | return """
60 |
61 | {{text + number}}
62 |
63 |
77 | """
78 |
79 | def vue_callback(self):
80 | self.callback()
81 |
82 |
83 | # We test that watch works for a purely Vue variable
84 | def test_watcher_vue(solara_test, page_session: Page):
85 | def callback():
86 | widget.text = "Clicked "
87 |
88 | widget = WatcherTemplateVue(callback=callback)
89 |
90 | display(widget)
91 |
92 | widget = page_session.locator("text=Click Me 0")
93 | widget.click()
94 | widget = page_session.locator("text=Clicked 1")
95 |
--------------------------------------------------------------------------------
/tests/unit/test_vue_widget.py:
--------------------------------------------------------------------------------
1 | from unittest.mock import MagicMock
2 |
3 | from ipyvue import VueWidget
4 |
5 |
6 | class TestVueWidget:
7 |
8 | CLASS = "tutu"
9 | CLASS_LIST = ["tutu", "toto"]
10 |
11 | def test_add_class(self):
12 |
13 | # empty widget
14 | test_widget = VueWidget()
15 | test_widget.class_list.add(*self.CLASS_LIST)
16 | assert test_widget.class_ == " ".join(self.CLASS_LIST)
17 |
18 | # with duplicate
19 | test_widget = VueWidget()
20 | test_widget.class_ = self.CLASS
21 | test_widget.class_list.add(*self.CLASS_LIST)
22 | assert test_widget.class_ == " ".join(self.CLASS_LIST)
23 |
24 | def test_remove_class(self):
25 |
26 | # existing
27 | test_widget = VueWidget()
28 | test_widget.class_ = " ".join(self.CLASS_LIST)
29 | test_widget.class_list.remove(self.CLASS)
30 | assert test_widget.class_ == self.CLASS_LIST[1]
31 |
32 | # not existing
33 | test_widget = VueWidget()
34 | test_widget.class_list.remove(*self.CLASS_LIST)
35 | assert test_widget.class_ == ""
36 |
37 | def test_toggle_class(self):
38 |
39 | test_widget = VueWidget()
40 | test_widget.class_ = self.CLASS
41 | test_widget.class_list.toggle(*self.CLASS_LIST)
42 | assert test_widget.class_ == self.CLASS_LIST[1]
43 |
44 | def test_replace_class(self):
45 |
46 | test_widget = VueWidget()
47 | test_widget.class_ = self.CLASS
48 | test_widget.class_list.replace(*self.CLASS_LIST)
49 | assert test_widget.class_ == self.CLASS_LIST[1]
50 |
51 | def test_hide(self):
52 |
53 | test_widget = VueWidget()
54 | test_widget.hide()
55 | assert "d-none" in test_widget.class_
56 |
57 | def test_show(self):
58 |
59 | test_widget = VueWidget()
60 | test_widget.class_list.add(self.CLASS, "d-none")
61 | test_widget.show()
62 | assert "d-none" not in test_widget.class_
63 |
64 |
65 | def test_event_handling():
66 | event_handler = MagicMock()
67 | button = VueWidget()
68 | button.on_event("click.stop", event_handler)
69 | button.fire_event("click.stop", {"foo": "bar"})
70 | event_handler.assert_called_once_with(button, "click.stop", {"foo": "bar"})
71 |
72 | event_handler.reset_mock()
73 | button.click({"foo": "bar"})
74 | event_handler.assert_called_once_with(button, "click.stop", {"foo": "bar"})
75 |
76 | # test the code path as if it's coming from the frontend
77 | event_handler.reset_mock()
78 | button._handle_event(None, dict(event="click.stop", data={"foo": "bar"}), [])
79 | event_handler.assert_called_once_with(button, "click.stop", {"foo": "bar"})
80 |
--------------------------------------------------------------------------------
/js/webpack.config.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 | var version = require('./package.json').version;
3 |
4 | module.exports = [
5 | {
6 | entry: './lib/extension.js',
7 | output: {
8 | filename: 'extension.js',
9 | path: path.resolve(__dirname, '..', 'ipyvue', 'nbextension'),
10 | libraryTarget: 'amd'
11 | },
12 | mode: 'production',
13 | },
14 | {
15 | entry: './lib/index.js',
16 | output: {
17 | filename: 'index.js',
18 | path: path.resolve(__dirname, '..', 'ipyvue', 'nbextension'),
19 | libraryTarget: 'amd'
20 | },
21 | devtool: 'source-map',
22 | externals: ['@jupyter-widgets/base'],
23 | mode: 'production',
24 | performance: {
25 | maxEntrypointSize: 1400000,
26 | maxAssetSize: 1400000
27 | },
28 | },
29 | {
30 | entry: './lib/nodeps.js',
31 | output: {
32 | filename: 'nodeps.js',
33 | path: path.resolve(__dirname, '..', 'ipyvue', 'nbextension'),
34 | libraryTarget: 'amd'
35 | },
36 | devtool: 'source-map',
37 | externals: ['@jupyter-widgets/base', 'vue'],
38 | mode: 'production',
39 | performance: {
40 | maxEntrypointSize: 1400000,
41 | maxAssetSize: 1400000
42 | },
43 | resolve: {
44 | alias: { './VueWithCompiler$': path.resolve(__dirname, 'src/nodepsVueWithCompiler.js') },
45 | },
46 | },
47 | {
48 | entry: './lib/nodeps.js',
49 | output: {
50 | filename: 'nodeps.js',
51 | path: path.resolve(__dirname, 'dist'),
52 | libraryTarget: 'amd',
53 | publicPath: 'https://unpkg.com/jupyter-vue@' + version + '/dist/'
54 | },
55 | devtool: 'source-map',
56 | externals: ['@jupyter-widgets/base', 'vue'],
57 | mode: 'production',
58 | performance: {
59 | maxEntrypointSize: 1400000,
60 | maxAssetSize: 1400000
61 | },
62 | resolve: {
63 | alias: { './VueWithCompiler$': path.resolve(__dirname, 'src/nodepsVueWithCompiler.js') },
64 | },
65 | },
66 | {
67 | entry: './lib/embed.js',
68 | output: {
69 | filename: 'index.js',
70 | path: path.resolve(__dirname, 'dist'),
71 | libraryTarget: 'amd',
72 | publicPath: 'https://unpkg.com/jupyter-vue@' + version + '/dist/'
73 | },
74 | devtool: 'source-map',
75 | externals: ['@jupyter-widgets/base'],
76 | mode: 'production',
77 | performance: {
78 | maxEntrypointSize: 1400000,
79 | maxAssetSize: 1400000
80 | },
81 | },
82 | ];
83 |
--------------------------------------------------------------------------------
/examples/EmbeddingJupyterWidgetsInVueTemplate.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "code",
5 | "execution_count": null,
6 | "metadata": {},
7 | "outputs": [],
8 | "source": [
9 | "import ipyvue as vue\n",
10 | "import ipywidgets as widgets\n",
11 | "from traitlets import (Unicode, List, Int, Bool, Any, Dict)\n",
12 | "\n",
13 | "slider1 = widgets.IntSlider(description='Slider 1', value=20)\n",
14 | "slider2 = widgets.IntSlider(description='Slider 2', value=40)\n",
15 | "\n",
16 | "class MyComponent(vue.VueTemplate):\n",
17 | " \n",
18 | " items = List([{\n",
19 | " 'title': 'Title 1',\n",
20 | " 'content': slider1\n",
21 | " }, {\n",
22 | " 'title': 'Title 2',\n",
23 | " 'content': slider2\n",
24 | " }]).tag(sync=True, **widgets.widget_serialization)\n",
25 | " \n",
26 | " single_widget = Any(\n",
27 | " widgets.IntSlider(description='Single slider', value=40)\n",
28 | " ).tag(sync=True, **widgets.widget_serialization)\n",
29 | "\n",
30 | "\n",
31 | " template=Unicode(\"\"\"\n",
32 | " \n",
33 | "
\n",
34 | " {{ item.title }}: \n",
35 | "
\n",
36 | " Single widget:
\n",
37 | "
\n",
38 | " \"\"\").tag(sync=True)\n",
39 | "\n",
40 | "my_component = MyComponent()\n",
41 | "my_component"
42 | ]
43 | },
44 | {
45 | "cell_type": "code",
46 | "execution_count": null,
47 | "metadata": {},
48 | "outputs": [],
49 | "source": [
50 | "# add a widget\n",
51 | "slider3 = widgets.IntSlider(description='Slider 3', value=60)\n",
52 | "my_component.items=[*my_component.items, {'title': 'Title 3', 'content': slider3}]"
53 | ]
54 | },
55 | {
56 | "cell_type": "code",
57 | "execution_count": null,
58 | "metadata": {},
59 | "outputs": [],
60 | "source": [
61 | "my_component.single_widget = widgets.IntSlider(description='changed')"
62 | ]
63 | },
64 | {
65 | "cell_type": "code",
66 | "execution_count": null,
67 | "metadata": {},
68 | "outputs": [],
69 | "source": []
70 | }
71 | ],
72 | "metadata": {
73 | "kernelspec": {
74 | "display_name": "Python 3",
75 | "language": "python",
76 | "name": "python3"
77 | },
78 | "language_info": {
79 | "codemirror_mode": {
80 | "name": "ipython",
81 | "version": 3
82 | },
83 | "file_extension": ".py",
84 | "mimetype": "text/x-python",
85 | "name": "python",
86 | "nbconvert_exporter": "python",
87 | "pygments_lexer": "ipython3",
88 | "version": "3.7.3"
89 | }
90 | },
91 | "nbformat": 4,
92 | "nbformat_minor": 2
93 | }
94 |
--------------------------------------------------------------------------------
/tests/ui/test_template.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | import sys
3 |
4 | if sys.version_info < (3, 7):
5 | pytest.skip("requires python3.7 or higher", allow_module_level=True)
6 |
7 | import ipyvue as vue
8 | import playwright.sync_api
9 |
10 | from IPython.display import display
11 | from traitlets import default, Int, Callable, Unicode
12 |
13 |
14 | class MyTemplate(vue.VueTemplate):
15 | clicks = Int(0).tag(sync=True)
16 |
17 | @default("template")
18 | def _default_vue_template(self):
19 | return """
20 |
21 | Clicked {{clicks}}
22 |
23 | """
24 |
25 |
26 | def test_template(ipywidgets_runner, page_session: playwright.sync_api.Page):
27 | def kernel_code():
28 | # this import is need so when this code executes in the kernel,
29 | # the class is imported
30 | from test_template import MyTemplate
31 |
32 | widget = MyTemplate()
33 | display(widget)
34 |
35 | ipywidgets_runner(kernel_code)
36 | widget = page_session.locator("text=Clicked 0")
37 | widget.wait_for()
38 | widget.click()
39 | page_session.locator("text=Clicked 1").wait_for()
40 |
41 |
42 | class MyEventTemplate(vue.VueTemplate):
43 | on_custom = Callable()
44 | text = Unicode("Click Me").tag(sync=True)
45 |
46 | @default("template")
47 | def _default_vue_template(self):
48 | return """
49 |
50 | {{text}}
51 |
52 | """
53 |
54 | def vue_custom_event(self, data):
55 | self.on_custom(data)
56 |
57 |
58 | def test_template_custom_event(solara_test, page_session: playwright.sync_api.Page):
59 | last_event_data = None
60 |
61 | def on_custom(data):
62 | nonlocal last_event_data
63 | last_event_data = data
64 | div.text = "Clicked"
65 |
66 | div = MyEventTemplate(on_custom=on_custom)
67 |
68 | display(div)
69 |
70 | page_session.locator("text=Click Me").click()
71 | page_session.locator("text=Clicked").wait_for()
72 | assert last_event_data == "not-an-event-object"
73 |
74 |
75 | class MyTemplateScript(vue.VueTemplate):
76 | clicks = Int(0).tag(sync=True)
77 |
78 | @default("template")
79 | def _default_vue_template(self):
80 | return """
81 |
82 | Clicked {{clicks}}
83 |
84 |
94 | """
95 |
96 |
97 | class MyTemplateScriptOld(vue.VueTemplate):
98 | clicks = Int(0).tag(sync=True)
99 |
100 | @default("template")
101 | def _default_vue_template(self):
102 | return """
103 |
104 | Clicked {{clicks}}
105 |
106 |
117 | """
118 |
119 |
120 | @pytest.mark.parametrize(
121 | "template_class_name", ["MyTemplateScript", "MyTemplateScriptOld"]
122 | )
123 | def test_template_script(
124 | ipywidgets_runner, page_session: playwright.sync_api.Page, template_class_name
125 | ):
126 | def kernel_code(template_class_name=template_class_name):
127 | # this import is need so when this code executes in the kernel,
128 | # the class is imported
129 | from test_template import MyTemplateScript, MyTemplateScriptOld
130 |
131 | template_class = {
132 | "MyTemplateScript": MyTemplateScript,
133 | "MyTemplateScriptOld": MyTemplateScriptOld,
134 | }[template_class_name]
135 |
136 | widget = template_class()
137 | display(widget)
138 |
139 | ipywidgets_runner(kernel_code, {"template_class_name": template_class_name})
140 | widget = page_session.locator("text=Clicked 0")
141 | widget.wait_for()
142 | widget.click()
143 | page_session.locator("text=Clicked 1").wait_for()
144 |
--------------------------------------------------------------------------------
/tests/ui/test_nested.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | import sys
3 | import re
4 |
5 | if sys.version_info < (3, 7):
6 | pytest.skip("requires python3.7 or higher", allow_module_level=True)
7 |
8 | import ipyvue as vue
9 | import playwright.sync_api
10 | from playwright.sync_api import expect
11 |
12 | from IPython.display import display
13 | from traitlets import default, Unicode, Instance
14 | import ipywidgets as widgets
15 |
16 |
17 | class MyTemplate(vue.VueTemplate):
18 | class_ = Unicode("template-parent").tag(sync=True)
19 | child = Instance(widgets.Widget, allow_none=True).tag(
20 | sync=True, **widgets.widget_serialization
21 | )
22 | text = Unicode(None, allow_none=True).tag(sync=True)
23 |
24 | @default("template")
25 | def _default_vue_template(self):
26 | return """
27 |
28 |
29 | {{text}}
30 |
31 |
32 |
33 | """
34 |
35 |
36 | @pytest.mark.parametrize("parent_is_template", [True, False])
37 | def test_vue_with_vue_widget_child(
38 | ipywidgets_runner, page_session: playwright.sync_api.Page, parent_is_template
39 | ):
40 | def kernel_code():
41 | from test_nested import MyTemplate
42 | import ipyvue as vue
43 |
44 | child = vue.Html(tag="div", children=["I am a widget sibling"])
45 |
46 | if parent_is_template:
47 | widget = MyTemplate(child=child, class_="test-parent")
48 | else:
49 | widget = vue.Html(
50 | tag="div",
51 | children=[child],
52 | class_="test-parent",
53 | )
54 | display(widget)
55 |
56 | ipywidgets_runner(kernel_code, {"parent_is_template": parent_is_template})
57 | parent = page_session.locator(".test-parent")
58 | parent.wait_for()
59 | expect(parent.locator(":nth-child(1)")).to_contain_text("I am a widget sibling")
60 |
61 |
62 | @pytest.mark.parametrize("parent_is_template", [True, False])
63 | def test_vue_with_vue_template_child(
64 | ipywidgets_runner, page_session: playwright.sync_api.Page, parent_is_template
65 | ):
66 | def kernel_code():
67 | # this import is need so when this code executes in the kernel,
68 | # the class is imported
69 | from test_nested import MyTemplate
70 | import ipyvue as vue
71 |
72 | child = MyTemplate(class_="test-child", text="I am a child")
73 |
74 | if parent_is_template:
75 | widget = MyTemplate(
76 | child=child,
77 | class_="test-parent",
78 | )
79 | else:
80 | widget = vue.Html(
81 | tag="div",
82 | children=[child],
83 | class_="test-parent",
84 | )
85 | display(widget)
86 |
87 | ipywidgets_runner(kernel_code, {"parent_is_template": parent_is_template})
88 | parent = page_session.locator(".test-parent")
89 | parent.wait_for()
90 | expect(parent.locator(":nth-child(1) >> nth=0")).to_have_class("test-child")
91 | expect(parent.locator(".test-child >> :nth-child(1)")).to_contain_text(
92 | "I am a child"
93 | )
94 |
95 |
96 | @pytest.mark.parametrize("parent_is_template", [True, False])
97 | def test_vue_with_ipywidgets_child(
98 | ipywidgets_runner, page_session: playwright.sync_api.Page, parent_is_template
99 | ):
100 | def kernel_code():
101 | from test_nested import MyTemplate
102 | import ipyvue as vue
103 | import ipywidgets as widgets
104 |
105 | child = widgets.Label(value="I am a child")
106 | child.add_class("widget-child")
107 |
108 | if parent_is_template:
109 | widget = MyTemplate(
110 | child=child,
111 | class_="test-parent",
112 | )
113 | else:
114 | widget = vue.Html(
115 | tag="div",
116 | children=[child],
117 | class_="test-parent",
118 | )
119 | display(widget)
120 |
121 | ipywidgets_runner(kernel_code, {"parent_is_template": parent_is_template})
122 | parent = page_session.locator(".test-parent")
123 | parent.wait_for()
124 | # extra div is created by ipyvue
125 | expect(parent.locator(":nth-child(1) >> :nth-child(1)")).to_have_class(
126 | re.compile(".*widget-child.*")
127 | )
128 | expect(parent.locator(".widget-child")).to_contain_text("I am a child")
129 |
130 |
131 | @pytest.mark.parametrize("parent_is_template", [True, False])
132 | def test_vue_ipywidgets_vue(
133 | ipywidgets_runner, page_session: playwright.sync_api.Page, parent_is_template
134 | ):
135 | # tests an interrupted vue hierarchy
136 | def kernel_code():
137 | import ipywidgets as widgets
138 | import ipyvue as vue
139 |
140 | child = vue.Html(
141 | tag="div", children=["I am a widget sibling"], class_="test-child"
142 | )
143 | parent = widgets.VBox(children=[child])
144 | parent.add_class("ipywidgets-parent")
145 | grant_parent = vue.Html(
146 | tag="div",
147 | children=[child],
148 | class_="test-grandparent",
149 | )
150 | display(grant_parent)
151 |
152 | ipywidgets_runner(kernel_code, {"parent_is_template": parent_is_template})
153 | grand_parent = page_session.locator(".test-grandparent")
154 | grand_parent.wait_for()
155 | expect(grand_parent.locator(".test-child")).to_contain_text("I am a widget sibling")
156 |
--------------------------------------------------------------------------------
/.github/workflows/unit.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | tags:
8 | - v*
9 | pull_request:
10 | workflow_dispatch:
11 |
12 | jobs:
13 | code-quality:
14 | runs-on: ubuntu-24.04
15 | steps:
16 | - uses: actions/checkout@v2
17 | - name: Install Python
18 | uses: actions/setup-python@v2
19 | with:
20 | python-version: "3.11"
21 | - name: Install dependencies
22 | run: pip install -r .github/requirements.txt
23 | - name: Run black
24 | run: black . --check
25 | - name: Setup flake8 annotations
26 | uses: rbialon/flake8-annotations@v1
27 |
28 | build:
29 | runs-on: ubuntu-24.04
30 | steps:
31 | - uses: actions/checkout@v2
32 |
33 | - name: Install node
34 | uses: actions/setup-node@v1
35 | with:
36 | node-version: "14.x"
37 | registry-url: "https://registry.npmjs.org"
38 |
39 | - name: Install Python
40 | uses: actions/setup-python@v2
41 | with:
42 | python-version: "3.11"
43 |
44 | - name: Install dependencies
45 | run: |
46 | python -m pip install --upgrade pip
47 | pip install twine wheel jupyter-packaging "jupyterlab<4"
48 |
49 | - name: Build
50 | run: |
51 | python setup.py sdist bdist_wheel
52 |
53 | - name: Build
54 | run: |
55 | cd js
56 | npm pack
57 |
58 | - name: Check number of files in wheel
59 | run: |
60 | wheel=(dist/*.whl)
61 | unzip -d count ${wheel}
62 | ls -1 count
63 | if [[ $(ls -1 count | wc -l) -ne 3 ]]; then echo "Expected 4 files/directory"; exit 1; fi
64 |
65 | - name: Upload builds
66 | uses: actions/upload-artifact@v4
67 | with:
68 | name: ipyvue-dist-${{ github.run_number }}
69 | path: |
70 | ./dist
71 | ./js/*.tgz
72 |
73 | test:
74 | needs: [build]
75 | runs-on: ubuntu-24.04
76 | strategy:
77 | fail-fast: false
78 | matrix:
79 | python-version: [3.8, 3.9, "3.10", "3.11"]
80 | steps:
81 | - uses: actions/checkout@v2
82 |
83 | - uses: actions/download-artifact@v4
84 | with:
85 | name: ipyvue-dist-${{ github.run_number }}
86 |
87 | - name: Install Python
88 | uses: actions/setup-python@v2
89 | with:
90 | python-version: ${{ matrix.python-version }}
91 |
92 | - name: Install dependencies
93 | run: pip install -r .github/requirements.txt
94 |
95 | - name: Install
96 | run: pip install dist/*.whl
97 |
98 | - name: test with pytest
99 | run: coverage run -m pytest --color=yes tests/unit
100 |
101 | - name: Import
102 | # do the import in a subdirectory, as after installation, files in de current directory are also imported
103 | run: |
104 | (mkdir test-install; cd test-install; python -c "from ipyvue import Html")
105 |
106 | ui-test:
107 | needs: [build]
108 | runs-on: ubuntu-24.04
109 | steps:
110 | - uses: actions/checkout@v2
111 |
112 | - uses: actions/download-artifact@v4
113 | with:
114 | name: ipyvue-dist-${{ github.run_number }}
115 |
116 | - name: Install Python
117 | uses: actions/setup-python@v2
118 | with:
119 | python-version: 3.8
120 |
121 | - name: Install vuetify and test deps
122 | run: |
123 | wheel=(dist/*.whl)
124 | pip install ${wheel}[test] "jupyter_server<2"
125 |
126 | - name: Install playwright browsers
127 | run: playwright install chromium
128 |
129 | - name: Run ui-tests
130 | run: pytest tests/ui/ --video=retain-on-failure --solara-update-snapshots-ci -s
131 |
132 | - name: Upload Test artifacts
133 | if: always()
134 | uses: actions/upload-artifact@v4
135 | with:
136 | name: ipyvue-test-results
137 | path: test-results
138 |
139 | release-dry-run:
140 | needs: [ test,ui-test,code-quality ]
141 | runs-on: ubuntu-24.04
142 | steps:
143 | - uses: actions/download-artifact@v4
144 | with:
145 | name: ipyvue-dist-${{ github.run_number }}
146 |
147 | - name: Install node
148 | uses: actions/setup-node@v1
149 | with:
150 | node-version: "14.x"
151 | registry-url: "https://registry.npmjs.org"
152 |
153 | - name: Publish the NPM package
154 | run: |
155 | cd js
156 | echo $PRE_RELEASE
157 | if [[ $PRE_RELEASE == "true" ]]; then export TAG="next"; else export TAG="latest"; fi
158 | npm publish --dry-run --tag ${TAG} --access public *.tgz
159 | env:
160 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
161 | PRE_RELEASE: ${{ github.event.release.prerelease }}
162 | release:
163 | if: startsWith(github.event.ref, 'refs/tags/v')
164 | needs: [release-dry-run]
165 | runs-on: ubuntu-24.04
166 | steps:
167 | - uses: actions/download-artifact@v4
168 | with:
169 | name: ipyvue-dist-${{ github.run_number }}
170 |
171 | - name: Install node
172 | uses: actions/setup-node@v1
173 | with:
174 | node-version: "14.x"
175 | registry-url: "https://registry.npmjs.org"
176 |
177 | - name: Install Python
178 | uses: actions/setup-python@v2
179 | with:
180 | python-version: 3.8
181 |
182 | - name: Install dependencies
183 | run: |
184 | python -m pip install --upgrade pip
185 | pip install twine wheel jupyter-packaging jupyterlab
186 |
187 | - name: Publish the Python package
188 | env:
189 | TWINE_USERNAME: __token__
190 | TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
191 | run: twine upload --skip-existing dist/*
192 |
193 | - name: Publish the NPM package
194 | run: |
195 | cd js
196 | echo $PRE_RELEASE
197 | if [[ $PRE_RELEASE == "true" ]]; then export TAG="next"; else export TAG="latest"; fi
198 | npm publish --tag ${TAG} --access public *.tgz
199 | env:
200 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
201 | PRE_RELEASE: ${{ github.event.release.prerelease }}
202 |
--------------------------------------------------------------------------------
/ipyvue/VueTemplateWidget.py:
--------------------------------------------------------------------------------
1 | import os
2 | from traitlets import Any, Unicode, List, Dict, Union, Instance
3 | from ipywidgets import DOMWidget
4 | from ipywidgets.widgets.widget import widget_serialization
5 |
6 | from .Template import Template, get_template
7 | from ._version import semver
8 | from .ForceLoad import force_load_instance
9 | import inspect
10 | from importlib import import_module
11 |
12 | OBJECT_REF = "objectRef"
13 | FUNCTION_REF = "functionRef"
14 |
15 |
16 | class Events(object):
17 | def __init__(self, **kwargs):
18 | self.on_msg(self._handle_event)
19 | self.events = [item[4:] for item in dir(self) if item.startswith("vue_")]
20 |
21 | def _handle_event(self, _, content, buffers):
22 | def resolve_ref(value):
23 | if isinstance(value, dict):
24 | if OBJECT_REF in value.keys():
25 | obj = getattr(self, value[OBJECT_REF])
26 | for path_item in value.get("path", []):
27 | obj = obj[path_item]
28 | return obj
29 | if FUNCTION_REF in value.keys():
30 | fn = getattr(self, value[FUNCTION_REF])
31 | args = value.get("args", [])
32 | kwargs = value.get("kwargs", {})
33 | return fn(*args, **kwargs)
34 | return value
35 |
36 | if "create_widget" in content.keys():
37 | module_name = content["create_widget"][0]
38 | class_name = content["create_widget"][1]
39 | props = {k: resolve_ref(v) for k, v in content["props"].items()}
40 | module = import_module(module_name)
41 | widget = getattr(module, class_name)(**props, model_id=content["id"])
42 | self._component_instances = [*self._component_instances, widget]
43 | elif "update_ref" in content.keys():
44 | widget = DOMWidget.widgets[content["id"]]
45 | prop = content["prop"]
46 | obj = resolve_ref(content["update_ref"])
47 | setattr(widget, prop, obj)
48 | elif "destroy_widget" in content.keys():
49 | self._component_instances = [
50 | w
51 | for w in self._component_instances
52 | if w.model_id != content["destroy_widget"]
53 | ]
54 | elif "event" in content.keys():
55 | event = content.get("event", "")
56 | data = content.get("data", {})
57 | if buffers:
58 | getattr(self, "vue_" + event)(data, buffers)
59 | else:
60 | getattr(self, "vue_" + event)(data)
61 |
62 |
63 | def _value_to_json(x, obj):
64 | if inspect.isclass(x):
65 | return {"class": [x.__module__, x.__name__], "props": x.class_trait_names()}
66 | return widget_serialization["to_json"](x, obj)
67 |
68 |
69 | def _class_to_json(x, obj):
70 | if not x:
71 | return widget_serialization["to_json"](x, obj)
72 | return {k: _value_to_json(v, obj) for k, v in x.items()}
73 |
74 |
75 | def as_refs(name, data):
76 | def to_ref_structure(obj, path):
77 | if isinstance(obj, list):
78 | return [
79 | to_ref_structure(item, [*path, index]) for index, item in enumerate(obj)
80 | ]
81 | if isinstance(obj, dict):
82 | return {k: to_ref_structure(v, [*path, k]) for k, v in obj.items()}
83 |
84 | # add object id to detect a new object in the same structure
85 | return {OBJECT_REF: name, "path": path, "id": id(obj)}
86 |
87 | return to_ref_structure(data, [])
88 |
89 |
90 | class VueTemplate(DOMWidget, Events):
91 |
92 | class_component_serialization = {
93 | "from_json": widget_serialization["to_json"],
94 | "to_json": _class_to_json,
95 | }
96 |
97 | # Force the loading of jupyter-vue before dependent extensions when in a static
98 | # context (embed, voila)
99 | _jupyter_vue = Any(force_load_instance, read_only=True).tag(
100 | sync=True, **widget_serialization
101 | )
102 |
103 | _model_name = Unicode("VueTemplateModel").tag(sync=True)
104 |
105 | _view_name = Unicode("VueView").tag(sync=True)
106 |
107 | _view_module = Unicode("jupyter-vue").tag(sync=True)
108 |
109 | _model_module = Unicode("jupyter-vue").tag(sync=True)
110 |
111 | _view_module_version = Unicode(semver).tag(sync=True)
112 |
113 | _model_module_version = Unicode(semver).tag(sync=True)
114 |
115 | template = Union([Instance(Template), Unicode()]).tag(
116 | sync=True, **widget_serialization
117 | )
118 |
119 | css = Unicode(None, allow_none=True).tag(sync=True)
120 |
121 | methods = Unicode(None, allow_none=True).tag(sync=True)
122 |
123 | data = Unicode(None, allow_none=True).tag(sync=True)
124 |
125 | events = List(Unicode(), allow_none=True).tag(sync=True)
126 |
127 | components = Dict(default_value=None, allow_none=True).tag(
128 | sync=True, **class_component_serialization
129 | )
130 |
131 | _component_instances = List().tag(sync=True, **widget_serialization)
132 |
133 | template_file = None
134 |
135 | def __init__(self, *args, **kwargs):
136 | if self.template_file:
137 | abs_path = ""
138 | if type(self.template_file) == str:
139 | abs_path = os.path.abspath(self.template_file)
140 | elif type(self.template_file) == tuple:
141 | rel_file, path = self.template_file
142 | abs_path = os.path.join(os.path.dirname(rel_file), path)
143 |
144 | self.template = get_template(abs_path)
145 |
146 | super().__init__(*args, **kwargs)
147 |
148 | sync_ref_traitlets = [
149 | v for k, v in self.traits().items() if "sync_ref" in v.metadata.keys()
150 | ]
151 |
152 | def create_ref_and_observe(traitlet):
153 | data = traitlet.get(self)
154 | ref_name = traitlet.name + "_ref"
155 | self.add_traits(
156 | **{ref_name: Any(as_refs(traitlet.name, data)).tag(sync=True)}
157 | )
158 |
159 | def on_ref_source_change(change):
160 | setattr(self, ref_name, as_refs(traitlet.name, change["new"]))
161 |
162 | self.observe(on_ref_source_change, traitlet.name)
163 |
164 | for traitlet in sync_ref_traitlets:
165 | create_ref_and_observe(traitlet)
166 |
167 |
168 | __all__ = ["VueTemplate"]
169 |
--------------------------------------------------------------------------------
/class_components.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "code",
5 | "execution_count": null,
6 | "metadata": {},
7 | "outputs": [],
8 | "source": [
9 | "import ipywidgets as widgets\n",
10 | "import ipyvue as vue\n",
11 | "from traitlets import (Dict, Unicode, List, Instance, observe)"
12 | ]
13 | },
14 | {
15 | "cell_type": "code",
16 | "execution_count": null,
17 | "metadata": {},
18 | "outputs": [],
19 | "source": [
20 | "class SubComponent(vue.VueTemplate):\n",
21 | " input = Unicode().tag(sync=True)\n",
22 | " something = Unicode('defaultvalue').tag(sync=True)\n",
23 | " template = Unicode('''\n",
24 | " \n",
25 | " [{{ something }}] input: {{ input }} \n",
26 | "
\n",
27 | " ''').tag(sync=True)\n",
28 | " \n",
29 | " def vue_append_one(self, *args):\n",
30 | " self.input = f'{self.input}1'\n",
31 | " \n",
32 | "SubComponent(input='some text') "
33 | ]
34 | },
35 | {
36 | "cell_type": "code",
37 | "execution_count": null,
38 | "metadata": {
39 | "scrolled": true
40 | },
41 | "outputs": [],
42 | "source": [
43 | "class MainComponent(vue.VueTemplate):\n",
44 | " texts = List(['xxxx', 'yyyy']).tag(sync=True)\n",
45 | " direct = Unicode('aaa').tag(sync=True)\n",
46 | " template = Unicode('''\n",
47 | " \n",
48 | "
\n",
49 | " \n",
50 | "
\n",
51 | "
\n",
52 | " \n",
53 | " \n",
54 | " \n",
55 | " ''').tag(sync=True)\n",
56 | " \n",
57 | " components=Dict({\n",
58 | " 'sub-component': SubComponent,\n",
59 | " 'w-button': widgets.Button\n",
60 | " }).tag(sync=True, **vue.VueTemplate.class_component_serialization)\n",
61 | "\n",
62 | "mainComponent = MainComponent()\n",
63 | "mainComponent"
64 | ]
65 | },
66 | {
67 | "cell_type": "code",
68 | "execution_count": null,
69 | "metadata": {},
70 | "outputs": [],
71 | "source": [
72 | "mainComponent.texts=['xxxx', 'zzzz']"
73 | ]
74 | },
75 | {
76 | "cell_type": "code",
77 | "execution_count": null,
78 | "metadata": {},
79 | "outputs": [],
80 | "source": [
81 | "mainComponent._component_instances"
82 | ]
83 | },
84 | {
85 | "cell_type": "code",
86 | "execution_count": null,
87 | "metadata": {},
88 | "outputs": [],
89 | "source": [
90 | "mainComponent.direct = 'bbb'"
91 | ]
92 | },
93 | {
94 | "cell_type": "markdown",
95 | "metadata": {},
96 | "source": [
97 | "# Non serializable properties"
98 | ]
99 | },
100 | {
101 | "cell_type": "code",
102 | "execution_count": null,
103 | "metadata": {},
104 | "outputs": [],
105 | "source": [
106 | "class Database():\n",
107 | " def __init__(self, url):\n",
108 | " self.url = url\n",
109 | "\n",
110 | "class DatabaseInfo(vue.VueTemplate):\n",
111 | " db = Instance(Database)\n",
112 | " info = Unicode().tag(sync=True)\n",
113 | " label = Unicode().tag(sync=True)\n",
114 | " \n",
115 | " template = Unicode('''\n",
116 | " \n",
117 | " [{{ label }}] Db URL: {{ info }}\n",
118 | "
\n",
119 | " ''').tag(sync=True)\n",
120 | " \n",
121 | " @observe('db')\n",
122 | " def db_changed(self, change):\n",
123 | " self.info = self.db.url"
124 | ]
125 | },
126 | {
127 | "cell_type": "code",
128 | "execution_count": null,
129 | "metadata": {},
130 | "outputs": [],
131 | "source": [
132 | "class MyApp(vue.VueTemplate):\n",
133 | " \n",
134 | " customer_db = Instance(Database).tag(sync_ref=True)\n",
135 | " supplier_db_collection = Dict().tag(sync_ref=True)\n",
136 | " \n",
137 | " template = Unicode('''\n",
138 | " \n",
139 | " \n",
140 | " \n",
141 | " \n",
146 | " \n",
147 | " \n",
148 | "
\n",
149 | " ''').tag(sync=True)\n",
150 | " \n",
151 | " components = Dict({\n",
152 | " 'database-info': DatabaseInfo\n",
153 | " }).tag(sync=True, **vue.VueTemplate.class_component_serialization)\n",
154 | " \n",
155 | " def db_factory(self, url):\n",
156 | " return Database(url)\n",
157 | "\n",
158 | "my_app = MyApp(\n",
159 | " customer_db = Database('localhost/customers'),\n",
160 | " supplier_db_collection = {'preferred': [Database('localhost/intel')]}\n",
161 | ")\n",
162 | "my_app "
163 | ]
164 | },
165 | {
166 | "cell_type": "code",
167 | "execution_count": null,
168 | "metadata": {},
169 | "outputs": [],
170 | "source": [
171 | "my_app.customer_db = Database('remote/customers_v2')"
172 | ]
173 | },
174 | {
175 | "cell_type": "code",
176 | "execution_count": null,
177 | "metadata": {},
178 | "outputs": [],
179 | "source": [
180 | "my_app.supplier_db_collection = {'preferred': [Database('remote/amd')]}"
181 | ]
182 | },
183 | {
184 | "cell_type": "code",
185 | "execution_count": null,
186 | "metadata": {},
187 | "outputs": [],
188 | "source": []
189 | }
190 | ],
191 | "metadata": {
192 | "kernelspec": {
193 | "display_name": "Python 3",
194 | "language": "python",
195 | "name": "python3"
196 | },
197 | "language_info": {
198 | "codemirror_mode": {
199 | "name": "ipython",
200 | "version": 3
201 | },
202 | "file_extension": ".py",
203 | "mimetype": "text/x-python",
204 | "name": "python",
205 | "nbconvert_exporter": "python",
206 | "pygments_lexer": "ipython3",
207 | "version": "3.7.3"
208 | }
209 | },
210 | "nbformat": 4,
211 | "nbformat_minor": 2
212 | }
213 |
--------------------------------------------------------------------------------
/examples/fullVueComponent.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "code",
5 | "execution_count": null,
6 | "metadata": {},
7 | "outputs": [],
8 | "source": [
9 | "import ipyvue as vue\n",
10 | "import traitlets"
11 | ]
12 | },
13 | {
14 | "cell_type": "code",
15 | "execution_count": null,
16 | "metadata": {},
17 | "outputs": [],
18 | "source": [
19 | "class Parent(vue.VueTemplate):\n",
20 | " template = traitlets.Unicode('''\n",
21 | " \n",
22 | " \n",
23 | "
parent
\n",
24 | "
\n",
25 | "
\n",
26 | " \n",
27 | " ''').tag(sync=True)\n",
28 | " \n",
29 | " myprop = traitlets.Unicode('hello').tag(sync=True)\n",
30 | " \n",
31 | " components = traitlets.Dict({'full-vue': '''\n",
32 | " \n",
33 | " \n",
34 | " Prop from parent: \n",
35 | "
{{ someProp}}
\n",
36 | " \n",
37 | " \n",
38 | " \n",
43 | " '''}).tag(sync=True)\n",
44 | "\n",
45 | "parent = Parent()\n",
46 | "parent"
47 | ]
48 | },
49 | {
50 | "cell_type": "code",
51 | "execution_count": null,
52 | "metadata": {},
53 | "outputs": [],
54 | "source": [
55 | "parent.myprop = 'hi'"
56 | ]
57 | },
58 | {
59 | "cell_type": "code",
60 | "execution_count": null,
61 | "metadata": {},
62 | "outputs": [],
63 | "source": [
64 | "vue.register_component_from_string(\n",
65 | " 'g-sub',\n",
66 | " '''\n",
67 | " \n",
68 | " sub-component\n",
69 | " \n",
83 | " \n",
84 | " Prop from parent (g): \n",
85 | "
{{ someProp}}
\n",
86 | "
\n",
87 | "
\n",
88 | " sub: \n",
89 | "
\n",
90 | "
\n",
91 | " \n",
92 | " \n",
102 | " \n",
107 | "''')"
108 | ]
109 | },
110 | {
111 | "cell_type": "code",
112 | "execution_count": null,
113 | "metadata": {},
114 | "outputs": [],
115 | "source": [
116 | "class GlobalExample(vue.VueTemplate):\n",
117 | " template = traitlets.Unicode('''\n",
118 | " \n",
119 | " \n",
120 | "
parent
\n",
121 | "
\n",
122 | "
\n",
123 | " \n",
124 | " ''').tag(sync=True)\n",
125 | " \n",
126 | " myprop = traitlets.Unicode('hello').tag(sync=True)\n",
127 | " \n",
128 | "globalExample = GlobalExample()\n",
129 | "globalExample\n"
130 | ]
131 | },
132 | {
133 | "cell_type": "code",
134 | "execution_count": null,
135 | "metadata": {},
136 | "outputs": [],
137 | "source": [
138 | "vue.Html(tag='h3', children=['test scoped'])"
139 | ]
140 | },
141 | {
142 | "cell_type": "code",
143 | "execution_count": null,
144 | "metadata": {},
145 | "outputs": [],
146 | "source": [
147 | "globalExample.myprop"
148 | ]
149 | },
150 | {
151 | "cell_type": "code",
152 | "execution_count": null,
153 | "metadata": {},
154 | "outputs": [],
155 | "source": [
156 | "globalExample.myprop = 'hi'"
157 | ]
158 | },
159 | {
160 | "cell_type": "code",
161 | "execution_count": null,
162 | "metadata": {},
163 | "outputs": [],
164 | "source": [
165 | "import ipyvuetify as v\n",
166 | "from traitlets import Unicode, Dict, List\n",
167 | "import ipywidgets as w\n",
168 | "\n",
169 | "class Testing(v.VuetifyTemplate):\n",
170 | " template = Unicode(\"\"\"\n",
171 | " \n",
172 | " \n",
173 | " \n",
174 | " \"\"\").tag(sync=True)\n",
175 | " \n",
176 | " components = Dict({\n",
177 | " 'test-comp': \"\"\"\n",
178 | " \n",
179 | " \n",
180 | " Test\n",
181 | " HERE\n",
182 | " {{ viewer }}\n",
183 | " \n",
184 | " \n",
185 | " \n",
186 | " \n",
191 | " \"\"\"\n",
192 | " }).tag(sync=True)\n",
193 | " \n",
194 | " some_widget = List([\n",
195 | " v.Btn(children=['Test'])\n",
196 | " ]).tag(sync=True, **w.widget_serialization)\n",
197 | " \n",
198 | "Testing()"
199 | ]
200 | },
201 | {
202 | "cell_type": "code",
203 | "execution_count": null,
204 | "metadata": {},
205 | "outputs": [],
206 | "source": []
207 | }
208 | ],
209 | "metadata": {
210 | "kernelspec": {
211 | "display_name": "Python 3",
212 | "language": "python",
213 | "name": "python3"
214 | },
215 | "language_info": {
216 | "codemirror_mode": {
217 | "name": "ipython",
218 | "version": 3
219 | },
220 | "file_extension": ".py",
221 | "mimetype": "text/x-python",
222 | "name": "python",
223 | "nbconvert_exporter": "python",
224 | "pygments_lexer": "ipython3",
225 | "version": "3.7.6"
226 | }
227 | },
228 | "nbformat": 4,
229 | "nbformat_minor": 2
230 | }
231 |
--------------------------------------------------------------------------------
/ipyvue/VueWidget.py:
--------------------------------------------------------------------------------
1 | from traitlets import Unicode, Instance, Union, List, Any, Dict
2 | from ipywidgets import DOMWidget
3 | from ipywidgets.widgets.widget_layout import Layout
4 | from ipywidgets.widgets.widget import widget_serialization, CallbackDispatcher
5 | from ipywidgets.widgets.trait_types import InstanceDict
6 |
7 | from ._version import semver
8 | from .ForceLoad import force_load_instance
9 |
10 |
11 | class ClassList:
12 | def __init__(self, obj):
13 | self.obj = obj
14 |
15 | def remove(self, *classes):
16 | """
17 | Remove class elements from the class_ trait of the linked object.
18 |
19 | :param *classes (str): The classes to remove
20 | """
21 |
22 | classes = [str(c) for c in classes]
23 |
24 | src_classes = self.obj.class_.split() if self.obj.class_ else []
25 | dst_classes = [c for c in src_classes if c not in classes]
26 |
27 | self.obj.class_ = " ".join(dst_classes)
28 |
29 | def add(self, *classes):
30 | """
31 | add class elements to the class_ trait of the linked object.
32 |
33 | :param *classes (str): The classes to add
34 | """
35 |
36 | classes = [str(c) for c in classes]
37 |
38 | src_classes = self.obj.class_.split() if self.obj.class_ else []
39 | dst_classes = src_classes + [c for c in classes if c not in src_classes]
40 |
41 | self.obj.class_ = " ".join(dst_classes)
42 |
43 | def toggle(self, *classes):
44 | """
45 | toggle class elements to the class_ trait of the linked object.
46 |
47 | :param *classes (str): The classes to toggle
48 | """
49 |
50 | classes = [str(c) for c in classes]
51 |
52 | src_classes = self.obj.class_.split() if self.obj.class_ else []
53 | dst_classes = [c for c in src_classes if c not in classes] + [
54 | c for c in classes if c not in src_classes
55 | ]
56 |
57 | self.obj.class_ = " ".join(dst_classes)
58 |
59 | def replace(self, src, dst):
60 | """
61 | Replace class element by another in the class_ trait of the linked object.
62 |
63 | :param (source, destination). If the source is not found nothing is done.
64 | """
65 | src_classes = self.obj.class_.split() if self.obj.class_ else []
66 |
67 | src = str(src)
68 | dst = str(dst)
69 |
70 | dst_classes = [dst if c == src else c for c in src_classes]
71 |
72 | self.obj.class_ = " ".join(dst_classes)
73 |
74 |
75 | class Events(object):
76 | def __init__(self, **kwargs):
77 | self._event_handlers_map = {}
78 | self.on_msg(self._handle_event)
79 |
80 | def on_event(self, event_and_modifiers, callback, remove=False):
81 | new_event = event_and_modifiers.split(".")[0]
82 | for existing_event in [
83 | event
84 | for event in self._event_handlers_map.keys()
85 | if event == new_event or event.startswith(new_event + ".")
86 | ]:
87 | del self._event_handlers_map[existing_event]
88 |
89 | self._event_handlers_map[event_and_modifiers] = CallbackDispatcher()
90 |
91 | self._event_handlers_map[event_and_modifiers].register_callback(
92 | callback, remove=remove
93 | )
94 |
95 | if remove and not self._event_handlers_map[event_and_modifiers].callbacks:
96 | del self._event_handlers_map[event_and_modifiers]
97 |
98 | difference = set(self._event_handlers_map.keys()) ^ set(self._events)
99 | if len(difference) != 0:
100 | self._events = list(self._event_handlers_map.keys())
101 |
102 | def fire_event(self, event, data=None):
103 | """Manually trigger an event handler on the Python side."""
104 | # note that a click event will trigger click.stop if that particular
105 | # event+modifier is registered.
106 | event_match = [
107 | k for k in self._event_handlers_map.keys() if k.startswith(event)
108 | ]
109 | if not event_match:
110 | raise ValueError(f"'{event}' not found in widget {self}")
111 |
112 | self._fire_event(event_match[0], data)
113 |
114 | def click(self, data=None):
115 | """Manually triggers the event handler for the 'click' event
116 |
117 | Note that this does not trigger a click event in the browser, this only
118 | invokes the Python event handlers.
119 | """
120 | self.fire_event("click", data or {})
121 |
122 | def _fire_event(self, event, data=None):
123 | dispatcher = self._event_handlers_map[event]
124 | # we don't call via the dispatcher, since that eats exceptions
125 | for callback in dispatcher.callbacks:
126 | callback(self, event, data)
127 |
128 | def _handle_event(self, _, content, buffers):
129 | event = content.get("event", "")
130 | data = content.get("data", {})
131 | self._fire_event(event, data)
132 |
133 |
134 | class VueWidget(DOMWidget, Events):
135 | # we can drop this when https://github.com/jupyter-widgets/ipywidgets/pull/3592
136 | # is merged
137 | layout = InstanceDict(Layout, allow_none=True).tag(
138 | sync=True, **widget_serialization
139 | )
140 |
141 | # Force the loading of jupyter-vue before dependent extensions when in a static
142 | # context (embed, voila)
143 | _jupyter_vue = Any(force_load_instance, read_only=True).tag(
144 | sync=True, **widget_serialization
145 | )
146 |
147 | _model_name = Unicode("VueModel").tag(sync=True)
148 |
149 | _view_name = Unicode("VueView").tag(sync=True)
150 |
151 | _view_module = Unicode("jupyter-vue").tag(sync=True)
152 |
153 | _model_module = Unicode("jupyter-vue").tag(sync=True)
154 |
155 | _view_module_version = Unicode(semver).tag(sync=True)
156 |
157 | _model_module_version = Unicode(semver).tag(sync=True)
158 |
159 | children = List(Union([Instance(DOMWidget), Unicode()])).tag(
160 | sync=True, **widget_serialization
161 | )
162 |
163 | slot = Unicode(None, allow_none=True).tag(sync=True)
164 |
165 | _events = List(Unicode()).tag(sync=True)
166 |
167 | v_model = Any("!!disabled!!", allow_none=True).tag(sync=True)
168 |
169 | style_ = Unicode(None, allow_none=True).tag(sync=True)
170 |
171 | class_ = Unicode(None, allow_none=True).tag(sync=True)
172 |
173 | attributes = Dict(None, allow_none=True).tag(sync=True)
174 |
175 | v_slots = List(Dict()).tag(sync=True, **widget_serialization)
176 |
177 | v_on = Unicode(None, allow_none=True).tag(sync=True)
178 |
179 | def __init__(self, **kwargs):
180 |
181 | self.class_list = ClassList(self)
182 |
183 | super().__init__(**kwargs)
184 |
185 | def show(self):
186 | """Make the widget visible"""
187 |
188 | self.class_list.remove("d-none")
189 |
190 | def hide(self):
191 | """Make the widget invisible"""
192 |
193 | self.class_list.add("d-none")
194 |
195 |
196 | __all__ = ["VueWidget"]
197 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from __future__ import print_function
2 |
3 | import glob
4 | import os
5 | import platform
6 | import sys
7 | from distutils import log
8 | from subprocess import CalledProcessError, check_call
9 |
10 | from setuptools import Command, find_packages, setup
11 | from setuptools.command.build_py import build_py
12 | from setuptools.command.develop import develop
13 | from setuptools.command.egg_info import egg_info
14 | from setuptools.command.sdist import sdist
15 |
16 | here = os.path.dirname(os.path.abspath(__file__))
17 | node_root = os.path.join(here, "js")
18 | is_repo = os.path.exists(os.path.join(here, ".git"))
19 |
20 | npm_path = os.pathsep.join(
21 | [
22 | os.path.join(node_root, "node_modules", ".bin"),
23 | os.environ.get("PATH", os.defpath),
24 | ]
25 | )
26 |
27 | LONG_DESCRIPTION = "Jupyter widgets base for Vue libraries"
28 |
29 |
30 | def get_data_files():
31 | tgz = "jupyter-vue-" + version_ns["__version__"] + ".tgz"
32 | return [
33 | ("share/jupyter/nbextensions/jupyter-vue", glob.glob("ipyvue/nbextension/*")),
34 | (
35 | "share/jupyter/labextensions/jupyter-vue",
36 | glob.glob("ipyvue/labextension/package.json"),
37 | ),
38 | (
39 | "share/jupyter/labextensions/jupyter-vue/static",
40 | glob.glob("ipyvue/labextension/static/*"),
41 | ),
42 | ("etc/jupyter/nbconfig/notebook.d", ["jupyter-vue.json"]),
43 | ("share/jupyter/lab/extensions", ["js/" + tgz]),
44 | ]
45 |
46 |
47 | def js_prerelease(command, strict=False):
48 | """decorator for building minified js/css prior to another command"""
49 |
50 | class DecoratedCommand(command):
51 | def run(self):
52 | jsdeps = self.distribution.get_command_obj("jsdeps")
53 | if not is_repo and all(os.path.exists(t) for t in jsdeps.targets):
54 | # sdist, nothing to do
55 | command.run(self)
56 | return
57 |
58 | try:
59 | self.distribution.run_command("jsdeps")
60 | except Exception as e:
61 | missing = [t for t in jsdeps.targets if not os.path.exists(t)]
62 | if strict or missing:
63 | log.warn("rebuilding js and css failed")
64 | if missing:
65 | log.error("missing files: %s" % missing)
66 | raise e
67 | else:
68 | log.warn("rebuilding js and css failed (not a problem)")
69 | log.warn(str(e))
70 | command.run(self)
71 | update_package_data(self.distribution)
72 |
73 | return DecoratedCommand
74 |
75 |
76 | def update_package_data(distribution):
77 | """update package_data to catch changes during setup"""
78 | build_py = distribution.get_command_obj("build_py")
79 | distribution.data_files = get_data_files()
80 | # re-init build_py options which load package_data
81 | build_py.finalize_options()
82 |
83 |
84 | class NPM(Command):
85 | description = "install package.json dependencies using npm"
86 |
87 | user_options = []
88 |
89 | node_modules = os.path.join(node_root, "node_modules")
90 |
91 | targets = [
92 | os.path.join(here, "ipyvue", "nbextension", "extension.js"),
93 | os.path.join(here, "ipyvue", "nbextension", "index.js"),
94 | ]
95 |
96 | def initialize_options(self):
97 | pass
98 |
99 | def finalize_options(self):
100 | pass
101 |
102 | def get_npm_name(self):
103 | npmName = "npm"
104 | if platform.system() == "Windows":
105 | npmName = "npm.cmd"
106 |
107 | return npmName
108 |
109 | def has_npm(self):
110 | npmName = self.get_npm_name()
111 | try:
112 | check_call([npmName, "--version"])
113 | return True
114 | except CalledProcessError:
115 | return False
116 |
117 | def should_run_npm_install(self):
118 | return self.has_npm()
119 |
120 | def run(self):
121 | has_npm = self.has_npm()
122 | if not has_npm:
123 | log.error(
124 | "`npm` unavailable. If you're running this command using sudo, "
125 | "make sure `npm` is available to sudo"
126 | )
127 |
128 | env = os.environ.copy()
129 | env["PATH"] = npm_path
130 |
131 | if self.should_run_npm_install():
132 | log.info(
133 | "Installing build dependencies with npm. This may take a while..."
134 | )
135 | npmName = self.get_npm_name()
136 | check_call(
137 | [npmName, "install"],
138 | cwd=node_root,
139 | stdout=sys.stdout,
140 | stderr=sys.stderr,
141 | )
142 | check_call(
143 | [npmName, "pack"], cwd=node_root, stdout=sys.stdout, stderr=sys.stderr
144 | )
145 | os.utime(self.node_modules, None)
146 |
147 | for t in self.targets:
148 | if not os.path.exists(t):
149 | msg = "Missing file: %s" % t
150 | if not has_npm:
151 | msg += (
152 | "\nnpm is required to build a development version of a"
153 | " widget extension"
154 | )
155 | raise ValueError(msg)
156 |
157 | # update package data in case this created new files
158 | update_package_data(self.distribution)
159 |
160 |
161 | class DevelopCmd(develop):
162 | def run(self):
163 | check_call(["pre-commit", "install"])
164 | super(DevelopCmd, self).run()
165 |
166 |
167 | version_ns = {}
168 | with open(os.path.join(here, "ipyvue", "_version.py")) as f:
169 | exec(f.read(), {}, version_ns)
170 |
171 | setup(
172 | name="ipyvue",
173 | version=version_ns["__version__"],
174 | description="Jupyter widgets base for Vue libraries",
175 | long_description=LONG_DESCRIPTION,
176 | include_package_data=True,
177 | data_files=get_data_files(),
178 | install_requires=[
179 | "ipywidgets>=7.0.0",
180 | ],
181 | extras_require={
182 | "test": [
183 | "solara[pytest]",
184 | ],
185 | "dev": [
186 | "pre-commit",
187 | ],
188 | },
189 | packages=find_packages(exclude=["tests", "tests.*"]),
190 | zip_safe=False,
191 | cmdclass={
192 | "build_py": js_prerelease(build_py),
193 | "egg_info": js_prerelease(egg_info),
194 | "sdist": js_prerelease(sdist, strict=True),
195 | "jsdeps": NPM,
196 | "develop": DevelopCmd,
197 | },
198 | license="MIT",
199 | author="Mario Buikhuizen, Maarten Breddels",
200 | author_email="mbuikhuizen@gmail.com, maartenbreddels@gmail.com",
201 | url="https://github.com/widgetti/ipyvue",
202 | keywords=[
203 | "ipython",
204 | "jupyter",
205 | "widgets",
206 | ],
207 | classifiers=[
208 | "Development Status :: 4 - Beta",
209 | "Framework :: IPython",
210 | "Intended Audience :: Developers",
211 | "Intended Audience :: Science/Research",
212 | "Topic :: Multimedia :: Graphics",
213 | "Programming Language :: Python :: 3",
214 | "Programming Language :: Python :: 3.8",
215 | "Programming Language :: Python :: 3.9",
216 | "Programming Language :: Python :: 3.10",
217 | "Programming Language :: Python :: 3.11",
218 | ],
219 | )
220 |
--------------------------------------------------------------------------------
/js/src/VueRenderer.js:
--------------------------------------------------------------------------------
1 | /* eslint camelcase: ['error', {allow: ['v_model']}] */
2 | import * as base from '@jupyter-widgets/base';
3 | import { vueTemplateRender } from './VueTemplateRenderer'; // eslint-disable-line import/no-cycle
4 | import { VueModel } from './VueModel';
5 | import { VueTemplateModel } from './VueTemplateModel';
6 | import Vue from './VueWithCompiler';
7 |
8 | const JupyterPhosphorWidget = base.JupyterPhosphorWidget || base.JupyterLuminoWidget;
9 |
10 | export function createObjectForNestedModel(model, parentView) {
11 | let currentView = null;
12 | let destroyed = false;
13 | return {
14 | mounted() {
15 | parentView
16 | .create_child_view(model)
17 | .then(view => {
18 | currentView = view;
19 | // since create view is async, the vue component might be destroyed before the view is created
20 | if(!destroyed) {
21 | if(JupyterPhosphorWidget && (view.pWidget || view.luminoWidget || view.lmWidget)) {
22 | JupyterPhosphorWidget.attach(view.pWidget || view.luminoWidget || view.lmWidget, this.$el);
23 | } else {
24 | console.error("Could not attach widget to DOM using Lumino or Phosphor. Fallback to normal DOM attach", JupyterPhosphorWidget, view.pWidget, view.luminoWidget, view.lmWidget);
25 | this.$el.appendChild(view.el);
26 |
27 | }
28 | } else {
29 | currentView.remove();
30 | }
31 | });
32 | },
33 | beforeDestroy() {
34 | if (currentView) {
35 | // In vue 3 we can use the beforeUnmount, which is called before the node is removed from the DOM
36 | // In vue 2, we are already disconnected from the document at this stage, which phosphor does not like.
37 | // In order to avoid an error in phosphor, we add the node to the body before removing it.
38 | // (current.remove triggers a phosphor detach)
39 | // To be sure we do not cause any flickering, we hide the node before moving it.
40 | const widget = currentView.pWidget || currentView.luminoWidget || currentView.lmWidget;
41 | widget.node.style.display = "none";
42 | document.body.appendChild(widget.node)
43 | currentView.remove();
44 | } else {
45 | destroyed = true;
46 | }
47 | },
48 | render(createElement) {
49 | return createElement('div', { style: { height: '100%' } });
50 | },
51 | };
52 | }
53 |
54 | // based on https://stackoverflow.com/a/58416333/5397207
55 | function pickSerializable(object, depth=0, max_depth=2) {
56 | // change max_depth to see more levels, for a touch event, 2 is good
57 | if (depth > max_depth)
58 | return 'Object';
59 |
60 | const obj = {};
61 | for (let key in object) {
62 | let value = object[key];
63 | if (value instanceof Node)
64 | // specify which properties you want to see from the node
65 | value = {id: value.id};
66 | else if (value instanceof Window)
67 | value = 'Window';
68 | else if (value instanceof Object)
69 | value = pickSerializable(value, depth+1, max_depth);
70 |
71 | obj[key] = value;
72 | }
73 |
74 | return obj;
75 | }
76 |
77 | export function eventToObject(event) {
78 | if (event instanceof Event) {
79 | return pickSerializable(event);
80 | }
81 | return event;
82 | }
83 |
84 | export function vueRender(createElement, model, parentView, slotScopes) {
85 | if (model instanceof VueTemplateModel) {
86 | return vueTemplateRender(createElement, model, parentView);
87 | }
88 | if (!(model instanceof VueModel)) {
89 | return createElement(createObjectForNestedModel(model, parentView));
90 | }
91 | const tag = model.getVueTag();
92 |
93 | const elem = createElement({
94 | data() {
95 | return {
96 | v_model: model.get('v_model'),
97 | };
98 | },
99 | created() {
100 | addListeners(model, this);
101 | },
102 | render(createElement2) {
103 | const element = createElement2(
104 | tag,
105 | createContent(createElement2, model, this, parentView, slotScopes),
106 | renderChildren(createElement2, model.get('children'), this, parentView, slotScopes),
107 | );
108 | updateCache(this);
109 | return element;
110 | },
111 | }, { ...model.get('slot') && { slot: model.get('slot') } });
112 |
113 | /* Impersonate the wrapped component (e.g. v-tabs uses this name to detect v-tab and
114 | * v-tab-item) */
115 | elem.componentOptions.Ctor.options.name = tag;
116 | return elem;
117 | }
118 |
119 | function addListeners(model, vueModel) {
120 | const listener = () => {
121 | vueModel.$forceUpdate();
122 | };
123 | const use = key => key === '_events' || (!key.startsWith('_') && !['v_model'].includes(key));
124 |
125 | model.keys()
126 | .filter(use)
127 | .forEach(key => model.on(`change:${key}`, listener));
128 |
129 | model.on('change:v_model', () => {
130 | if (vueModel.v_model === "!!disabled!!") {
131 | vueModel.$forceUpdate();
132 | }
133 | if (model.get('v_model') !== vueModel.v_model) {
134 | vueModel.v_model = model.get('v_model'); // eslint-disable-line no-param-reassign
135 | }
136 | });
137 | }
138 |
139 | function createAttrsMapping(model) {
140 | const useAsAttr = key => model.get(key) !== null
141 | && !key.startsWith('_')
142 | && !['attributes', 'v_slots', 'v_on', 'layout', 'children', 'slot', 'v_model', 'style_', 'class_'].includes(key);
143 |
144 | return model.keys()
145 | .filter(useAsAttr)
146 | .reduce((result, key) => {
147 | result[key.replace(/_$/g, '').replace(/_/g, '-')] = model.get(key); // eslint-disable-line no-param-reassign
148 | return result;
149 | }, {});
150 | }
151 |
152 | function addEventWithModifiers(eventAndModifiers, obj, fn) { // eslint-disable-line no-unused-vars
153 | /* Example Vue.compile output:
154 | * (function anonymous() {
155 | * with (this) {
156 | * return _c('dummy', {
157 | * on: {
158 | * "[event]": function ($event) {
159 | * if (!$event.type.indexOf('key') && _k($event.keyCode, "c", ...)
160 | * return null;
161 | * ...
162 | * return [fn]($event)
163 | * }
164 | * }
165 | * })
166 | * }
167 | * }
168 | * )
169 | */
170 | const { on } = Vue.compile(``)
171 | .render.bind({
172 | _c: (_, data) => data,
173 | _k: Vue.prototype._k,
174 | fn,
175 | })();
176 |
177 | return {
178 | ...obj,
179 | ...on,
180 | };
181 | }
182 |
183 | function createEventMapping(model, parentView) {
184 | return (model.get('_events') || [])
185 | .reduce((result, eventAndModifiers) => addEventWithModifiers(
186 | eventAndModifiers,
187 | result,
188 | (e) => {
189 | model.send({
190 | event: eventAndModifiers,
191 | data: eventToObject(e),
192 | },
193 | model.callbacks(parentView));
194 | },
195 | ), {});
196 | }
197 |
198 | function createSlots(createElement, model, vueModel, parentView, slotScopes) {
199 | const slots = model.get('v_slots');
200 | if (!slots) {
201 | return undefined;
202 | }
203 | return slots.map(slot => ({
204 | key: slot.name,
205 | ...!slot.variable && { proxy: true },
206 | fn(slotScope) {
207 | return renderChildren(createElement,
208 | Array.isArray(slot.children) ? slot.children : [slot.children],
209 | vueModel, parentView, {
210 | ...slotScopes,
211 | ...slot.variable && { [slot.variable]: slotScope },
212 | });
213 | },
214 | }));
215 | }
216 |
217 | function getScope(value, slotScopes) {
218 | const parts = value.split('.');
219 | return parts
220 | .slice(1)
221 | .reduce(
222 | (scope, name) => scope[name],
223 | slotScopes[parts[0]],
224 | );
225 | }
226 |
227 | function getScopes(value, slotScopes) {
228 | return typeof value === 'string'
229 | ? getScope(value, slotScopes)
230 | : Object.assign({}, ...value.map(v => getScope(v, slotScopes)));
231 | }
232 |
233 | function slotUseOn(model, slotScopes) {
234 | const vOnValue = model.get('v_on');
235 | return vOnValue && getScopes(vOnValue, slotScopes);
236 | }
237 |
238 | function createContent(createElement, model, vueModel, parentView, slotScopes) {
239 | const htmlEventAttributes = model.get('attributes') && Object.keys(model.get('attributes')).filter(key => key.startsWith('on'));
240 | if (htmlEventAttributes && htmlEventAttributes.length > 0) {
241 | throw new Error(`No HTML event attributes may be used: ${htmlEventAttributes}`);
242 | }
243 |
244 | const scopedSlots = createSlots(createElement, model, vueModel, parentView, slotScopes);
245 |
246 | return {
247 | on: { ...createEventMapping(model, parentView), ...slotUseOn(model, slotScopes) },
248 | ...model.get('style_') && { style: model.get('style_') },
249 | ...model.get('class_') && { class: model.get('class_') },
250 | ...scopedSlots && { scopedSlots: vueModel._u(scopedSlots) },
251 | attrs: {
252 | ...createAttrsMapping(model),
253 | ...model.get('attributes') && model.get('attributes'),
254 | },
255 | ...model.get('v_model') !== '!!disabled!!' && {
256 | model: {
257 | value: vueModel.v_model,
258 | callback: (v) => {
259 | model.set('v_model', v === undefined ? null : v);
260 | model.save_changes(model.callbacks(parentView));
261 | },
262 | expression: 'v_model',
263 | },
264 | },
265 | };
266 | }
267 |
268 | function renderChildren(createElement, children, vueModel, parentView, slotScopes) {
269 | if (!vueModel.childCache) {
270 | vueModel.childCache = {}; // eslint-disable-line no-param-reassign
271 | }
272 | if (!vueModel.childIds) {
273 | vueModel.childIds = []; // eslint-disable-line no-param-reassign
274 | }
275 | const childViewModels = children.map((child) => {
276 | if (typeof (child) === 'string') {
277 | return child;
278 | }
279 | vueModel.childIds.push(child.cid);
280 |
281 | if (vueModel.childCache[child.cid]) {
282 | return vueModel.childCache[child.cid];
283 | }
284 | const vm = vueRender(createElement, child, parentView, slotScopes);
285 | vueModel.childCache[child.cid] = vm; // eslint-disable-line no-param-reassign
286 | return vm;
287 | });
288 |
289 | return childViewModels;
290 | }
291 |
292 | function updateCache(vueModel) {
293 | Object.keys(vueModel.childCache)
294 | .filter(key => !vueModel.childIds.includes(key))
295 | // eslint-disable-next-line no-param-reassign
296 | .forEach(key => delete vueModel.childCache[key]);
297 | vueModel.childIds = []; // eslint-disable-line no-param-reassign
298 | }
299 |
--------------------------------------------------------------------------------
/js/src/httpVueLoader.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | /* copied from https://github.com/FranckFreiburger/http-vue-loader and modified to load a component from a string */
3 | 'use strict';
4 |
5 | var scopeIndex = 0;
6 |
7 | StyleContext.prototype = {
8 |
9 | withBase: function(callback) {
10 |
11 | var tmpBaseElt;
12 | if ( this.component.baseURI ) {
13 |
14 | // firefox and chrome need the to be set while inserting or modifying