diff --git a/.eslintrc.json b/.eslintrc.json
new file mode 100644
index 0000000..c0ac4e5
--- /dev/null
+++ b/.eslintrc.json
@@ -0,0 +1,13 @@
+{
+ "env": {
+ "browser": true,
+ "node": true,
+ "es2021": true
+ },
+ "extends": ["eslint:recommended"],
+ "overrides": [],
+ "parserOptions": {
+ "ecmaVersion": "latest",
+ "sourceType": "module"
+ }
+}
diff --git a/notebooks/introduction.ipynb b/notebooks/introduction.ipynb
index 7b1f96f..59b56e5 100644
--- a/notebooks/introduction.ipynb
+++ b/notebooks/introduction.ipynb
@@ -4,43 +4,11 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "# What is ReactPy?\n",
+ "
\n",
"\n",
- "ReactPy connects your Python web framework of choice to a ReactJS frontend, allowing you to create **interactive websites without needing JavaScript!**\n",
+ "---\n",
"\n",
- "Following ReactJS styling, web elements are combined into [reusable \"components\"](https://reactpy.dev/docs/guides/creating-interfaces/your-first-components/index.html#parametrizing-components). These components can utilize [hooks](https://reactpy.dev/docs/reference/hooks-api.html) and [events](https://reactpy.dev/docs/guides/adding-interactivity/responding-to-events/index.html#async-event-handlers) to create infinitely complex web pages.\n",
- "\n",
- "When needed, ReactPy can [use components directly from NPM](https://reactpy.dev/docs/guides/escape-hatches/javascript-components.html#dynamically-loaded-components). For additional flexibility, components can also be [fully developed in JavaScript](https://reactpy.dev/docs/guides/escape-hatches/javascript-components.html#custom-javascript-components).\n",
- "\n",
- "\n",
- "# Getting Started\n",
- "\n",
- "Then, before anything else, do one of the following:\n",
- "\n",
- "1. At the top of your notebook run\n",
- "\n",
- " ```python\n",
- " import reactpy_jupyter\n",
- " ```\n",
- "\n",
- "2. Register `reactpy_jupyter` as a permanant IPython extension in [your config file](https://ipython.readthedocs.io/en/stable/config/intro.html#introduction-to-ipython-configuration):\n",
- "\n",
- " ```python\n",
- " c.InteractiveShellApp.extensions = ['reactpy_jupyter']\n",
- " ```\n",
- "\n",
- "For the purposes of this tutorial, you'll want to do the first:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {
- "tags": []
- },
- "outputs": [],
- "source": [
- "import reactpy_jupyter"
+ "[ReactPy](https://reactpy.dev/) is a library for building user interfaces in Python without Javascript. ReactPy interfaces are made from components which look and behave similarly to those found in [ReactJS](https://reactjs.org/). Designed with simplicity in mind, ReactPy can be used by those without web development experience while also being powerful enough to grow with your ambitions."
]
},
{
@@ -54,13 +22,29 @@
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 1,
"metadata": {
"tags": []
},
- "outputs": [],
+ "outputs": [
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "e0c3c63d3e1c4d88a71bb7bbc6ae45e0",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "LayoutWidget(Layout(ContextProvider()))"
+ ]
+ },
+ "execution_count": 1,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
"source": [
- "from reactpy import component, html, run\n",
+ "from reactpy import component, html\n",
"\n",
"\n",
"@component\n",
@@ -84,11 +68,27 @@
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 2,
"metadata": {
"tags": []
},
- "outputs": [],
+ "outputs": [
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "a87baeb77dcc44e1a29ab154df1dd8a7",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "LayoutWidget(Layout(ContextProvider()))"
+ ]
+ },
+ "execution_count": 2,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
"source": [
"from reactpy import component, html\n",
"\n",
@@ -137,11 +137,27 @@
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 3,
"metadata": {
"tags": []
},
- "outputs": [],
+ "outputs": [
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "920e5f89aba644528257e20deae25f2e",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "LayoutWidget(Layout(ContextProvider()))"
+ ]
+ },
+ "execution_count": 3,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
"source": [
"import json\n",
"from pathlib import Path\n",
@@ -186,22 +202,62 @@
"source": [
"# Using ReactPy With Jupyter Widgets\n",
"\n",
- "While you can use ReactPy components independently, it may also be useful to integrate them with the rest of the Jupyter Widget ecosystem. Let's consider a ReactPy component that responds to and displays changes from an `ipywidgets.IntSlider`. The ReactPy component will need to accept an `IntSlider` instance as one of its arguments, declare state that will track the slider's value, and register a lister that will update that state via the slider's `IntSlider.observe()` method using an [\"effect\"](https://reactpy.dev/docs/reference/hooks-api.html#use-effect):"
+ "It's possible to use Jupyter Widgets in ReactPy components if you convert them first using `reactpy_jupyter.from_widget`."
]
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 4,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "67cb81abf4ce4c9e991a9771607b9d5e",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "LayoutWidget(Layout(ContextProvider()))"
+ ]
+ },
+ "execution_count": 4,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "from reactpy_jupyter import from_widget\n",
+ "from ipywidgets import IntSlider\n",
+ "\n",
+ "slider_widget = IntSlider()\n",
+ "slider_component = from_widget(slider_widget)\n",
+ "\n",
+ "slider_component"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Let's consider a ReactPy component that responds to and displays changes from an `ipywidgets.IntSlider`. The ReactPy component will need to accept an `IntSlider` instance as one of its arguments, convert it to a component with `from_widget`, declare state that will track the slider's value, and register a lister that will update that state via the slider's `IntSlider.observe()` method using an [\"effect\"](https://reactpy.dev/docs/reference/hooks-api.html#use-effect):"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"from reactpy import use_effect\n",
+ "from reactpy_jupyter import from_widget\n",
"\n",
"\n",
"@component\n",
"def SliderObserver(slider):\n",
+ " slider_component = from_widget(slider)\n",
" value, set_value = use_state(0)\n",
"\n",
" @use_effect\n",
@@ -214,63 +270,122 @@
" # unobserve the slider's value if this component is no longer displayed\n",
" return lambda: slider.unobserve(handle_change, \"value\")\n",
"\n",
- " return html.p(f\"ReactPy observes the value to be: \", value)"
+ " return html.div(\n",
+ " slider_component, html.p(f\"ReactPy observes the value to be: \", value)\n",
+ " )"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "Now you'll need to display the `SliderObserver` component as well as an `IntSlider` widget. To do this, you'll want wrap the component in a `reactpy_jupyter.LayoutWidget` instance before using it alongside other Jupyter Widgets. Specifically, you'll be displaying the `SliderObserver` and `IntSlider` in a `Box`:\n"
+ "Now you need to pass the `SliderObserver` component an `IntSlider` widget and display it.\n"
]
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 6,
"metadata": {
"tags": []
},
- "outputs": [],
+ "outputs": [
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "c586a0f0fe0b44a3bab173ccb9fa52d8",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "LayoutWidget(Layout(ContextProvider()))"
+ ]
+ },
+ "execution_count": 6,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
"source": [
- "from ipywidgets import Box, IntSlider\n",
- "from reactpy_jupyter import LayoutWidget\n",
- "\n",
- "slider = IntSlider(readout=False)\n",
- "slider_observer = LayoutWidget(SliderObserver(slider))\n",
+ "from ipywidgets import IntSlider\n",
"\n",
- "Box([slider, slider_observer])"
+ "SliderObserver(IntSlider(readout=False))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "If it becomes painful to wrap every ReactPy component in a `LayoutWidget` you can create an alternate `LayoutWidget` constructor using `reactpy_jupyter.widgetize`:"
+ "You can also include ReactPy components within Jupyter Widgets using `reactpy_jupyter.to_widget`"
]
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 8,
"metadata": {
"tags": []
},
- "outputs": [],
+ "outputs": [
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "a0180ea4781b4dda9f77d1be75fbcad3",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Box(children=(IntSlider(value=0, readout=False), LayoutWidget(Layout(ContextProvider())), LayoutWidget(Layout…"
+ ]
+ },
+ "execution_count": 9,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "slider = IntSlider(readout=False)\n",
+ "slider_observer_constructor = to_widget(SliderObserver)\n",
+ "observer_1 = slider_observer_constructor(slider)\n",
+ "observer_2 = slider_observer_constructor(slider)\n",
+ "\n",
+ "Box([observer_1, observer_2])"
]
},
{
diff --git a/noxfile.py b/noxfile.py
index 053460b..b3a09b0 100644
--- a/noxfile.py
+++ b/noxfile.py
@@ -3,6 +3,20 @@
group = NoxOpt(auto_tag=True)
+@group.session
+def fix_lint(session: Session) -> None:
+ session.install(
+ "black[jupyter]",
+ "flake8-pyproject",
+ "flake8",
+ "isort",
+ )
+ session.run("black", ".")
+ session.run("isort", ".")
+
+ session.run("npm", "run", "fix:lint", external=True)
+
+
@group.session
def check_python(session: Session) -> None:
session.install(
diff --git a/package-lock.json b/package-lock.json
index bdcaf28..8082e81 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16,8 +16,7 @@
"@preact/preset-vite": "^2.5.0",
"@types/react": "<18",
"@types/react-dom": "<18",
- "eslint": "^8.38.0",
- "eslint-plugin-jsdoc": "^41.1.1",
+ "eslint": "^8.40.0",
"prettier": "^2.8.7",
"typescript": "^5.0.2",
"vite": "^4.2.1"
@@ -396,20 +395,6 @@
"node": ">=6.9.0"
}
},
- "node_modules/@es-joy/jsdoccomment": {
- "version": "0.37.0",
- "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.37.0.tgz",
- "integrity": "sha512-hjK0wnsPCYLlF+HHB4R/RbUjOWeLW2SlarB67+Do5WsKILOkmIZvvPJFbtWSmbypxcjpoECLAMzoao0D4Bg5ZQ==",
- "dev": true,
- "dependencies": {
- "comment-parser": "1.3.1",
- "esquery": "^1.4.0",
- "jsdoc-type-pratt-parser": "~4.0.0"
- },
- "engines": {
- "node": "^14 || ^16 || ^17 || ^18 || ^19"
- }
- },
"node_modules/@esbuild/android-arm": {
"version": "0.17.16",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.16.tgz",
@@ -778,23 +763,23 @@
}
},
"node_modules/@eslint-community/regexpp": {
- "version": "4.5.0",
- "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.0.tgz",
- "integrity": "sha512-vITaYzIcNmjn5tF5uxcZ/ft7/RXGrMUIS9HalWckEOF6ESiwXKoMzAQf2UW0aVd6rnOeExTJVd5hmWXucBKGXQ==",
+ "version": "4.5.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.1.tgz",
+ "integrity": "sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==",
"dev": true,
"engines": {
"node": "^12.0.0 || ^14.0.0 || >=16.0.0"
}
},
"node_modules/@eslint/eslintrc": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.2.tgz",
- "integrity": "sha512-3W4f5tDUra+pA+FzgugqL2pRimUTDJWKr7BINqOpkZrC0uYI0NIc0/JFgBROCU07HR6GieA5m3/rsPIhDmCXTQ==",
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.3.tgz",
+ "integrity": "sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ==",
"dev": true,
"dependencies": {
"ajv": "^6.12.4",
"debug": "^4.3.2",
- "espree": "^9.5.1",
+ "espree": "^9.5.2",
"globals": "^13.19.0",
"ignore": "^5.2.0",
"import-fresh": "^3.2.1",
@@ -825,9 +810,9 @@
}
},
"node_modules/@eslint/js": {
- "version": "8.38.0",
- "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.38.0.tgz",
- "integrity": "sha512-IoD2MfUnOV58ghIHCiil01PcohxjbYR/qCxsoC+xNgUwh1EY8jOOrYmu3d3a71+tJJ23uscEV4X2HJWMsPJu4g==",
+ "version": "8.40.0",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.40.0.tgz",
+ "integrity": "sha512-ElyB54bJIhXQYVKjDSvCkPO1iU1tSAeVQJbllWJq1XQSmmA4dgFk8CbiBGpiOPxleE48vDogxCtmMYku4HSVLA==",
"dev": true,
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
@@ -1377,15 +1362,6 @@
"node": ">=4"
}
},
- "node_modules/are-docs-informative": {
- "version": "0.0.2",
- "resolved": "https://registry.npmjs.org/are-docs-informative/-/are-docs-informative-0.0.2.tgz",
- "integrity": "sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==",
- "dev": true,
- "engines": {
- "node": ">=14"
- }
- },
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@@ -1511,15 +1487,6 @@
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
"dev": true
},
- "node_modules/comment-parser": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.3.1.tgz",
- "integrity": "sha512-B52sN2VNghyq5ofvUsqZjmk6YkihBX5vMSChmSK9v4ShjKf3Vk5Xcmgpw4o+iIgtrnM/u5FiMpz9VKb8lpBveA==",
- "dev": true,
- "engines": {
- "node": ">= 12.0.0"
- }
- },
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -1656,15 +1623,15 @@
}
},
"node_modules/eslint": {
- "version": "8.38.0",
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.38.0.tgz",
- "integrity": "sha512-pIdsD2jwlUGf/U38Jv97t8lq6HpaU/G9NKbYmpWpZGw3LdTNhZLbJePqxOXGB5+JEKfOPU/XLxYxFh03nr1KTg==",
+ "version": "8.40.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.40.0.tgz",
+ "integrity": "sha512-bvR+TsP9EHL3TqNtj9sCNJVAFK3fBN8Q7g5waghxyRsPLIMwL73XSKnZFK0hk/O2ANC+iAoq6PWMQ+IfBAJIiQ==",
"dev": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.4.0",
- "@eslint/eslintrc": "^2.0.2",
- "@eslint/js": "8.38.0",
+ "@eslint/eslintrc": "^2.0.3",
+ "@eslint/js": "8.40.0",
"@humanwhocodes/config-array": "^0.11.8",
"@humanwhocodes/module-importer": "^1.0.1",
"@nodelib/fs.walk": "^1.2.8",
@@ -1674,9 +1641,9 @@
"debug": "^4.3.2",
"doctrine": "^3.0.0",
"escape-string-regexp": "^4.0.0",
- "eslint-scope": "^7.1.1",
- "eslint-visitor-keys": "^3.4.0",
- "espree": "^9.5.1",
+ "eslint-scope": "^7.2.0",
+ "eslint-visitor-keys": "^3.4.1",
+ "espree": "^9.5.2",
"esquery": "^1.4.2",
"esutils": "^2.0.2",
"fast-deep-equal": "^3.1.3",
@@ -1712,73 +1679,6 @@
"url": "https://opencollective.com/eslint"
}
},
- "node_modules/eslint-plugin-jsdoc": {
- "version": "41.1.1",
- "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-41.1.1.tgz",
- "integrity": "sha512-dfH97DKLGtQ5dgEMzd+GSUuY+xX/yyAfjML3O0pEWmMMpylsG6Ro65s4ziYXKmixiENYK9CTQxCVRGqZUFN2Mw==",
- "dev": true,
- "dependencies": {
- "@es-joy/jsdoccomment": "~0.37.0",
- "are-docs-informative": "^0.0.2",
- "comment-parser": "1.3.1",
- "debug": "^4.3.4",
- "escape-string-regexp": "^4.0.0",
- "esquery": "^1.5.0",
- "semver": "^7.3.8",
- "spdx-expression-parse": "^3.0.1"
- },
- "engines": {
- "node": "^14 || ^16 || ^17 || ^18 || ^19"
- },
- "peerDependencies": {
- "eslint": "^7.0.0 || ^8.0.0"
- }
- },
- "node_modules/eslint-plugin-jsdoc/node_modules/escape-string-regexp": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
- "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
- "dev": true,
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/eslint-plugin-jsdoc/node_modules/lru-cache": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
- "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
- "dev": true,
- "dependencies": {
- "yallist": "^4.0.0"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/eslint-plugin-jsdoc/node_modules/semver": {
- "version": "7.4.0",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.4.0.tgz",
- "integrity": "sha512-RgOxM8Mw+7Zus0+zcLEUn8+JfoLpj/huFTItQy2hsM4khuC1HYRDp0cU482Ewn/Fcy6bCjufD8vAj7voC66KQw==",
- "dev": true,
- "dependencies": {
- "lru-cache": "^6.0.0"
- },
- "bin": {
- "semver": "bin/semver.js"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/eslint-plugin-jsdoc/node_modules/yallist": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
- "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
- "dev": true
- },
"node_modules/eslint-scope": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz",
@@ -1796,9 +1696,9 @@
}
},
"node_modules/eslint-visitor-keys": {
- "version": "3.4.0",
- "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.0.tgz",
- "integrity": "sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ==",
+ "version": "3.4.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz",
+ "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==",
"dev": true,
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
@@ -1905,14 +1805,14 @@
}
},
"node_modules/espree": {
- "version": "9.5.1",
- "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.1.tgz",
- "integrity": "sha512-5yxtHSZXRSW5pvv3hAlXM5+/Oswi1AUFqBmbibKb5s6bp3rGIDkyXU6xCoyuuLhijr4SFwPrXRoZjz0AZDN9tg==",
+ "version": "9.5.2",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.2.tgz",
+ "integrity": "sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw==",
"dev": true,
"dependencies": {
"acorn": "^8.8.0",
"acorn-jsx": "^5.3.2",
- "eslint-visitor-keys": "^3.4.0"
+ "eslint-visitor-keys": "^3.4.1"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
@@ -2287,15 +2187,6 @@
"js-yaml": "bin/js-yaml.js"
}
},
- "node_modules/jsdoc-type-pratt-parser": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.0.0.tgz",
- "integrity": "sha512-YtOli5Cmzy3q4dP26GraSOeAhqecewG04hoO8DY56CH4KJ9Fvv5qKWUCCo3HZob7esJQHCv6/+bnTy72xZZaVQ==",
- "dev": true,
- "engines": {
- "node": ">=12.0.0"
- }
- },
"node_modules/jsesc": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
@@ -2881,28 +2772,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/spdx-exceptions": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz",
- "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==",
- "dev": true
- },
- "node_modules/spdx-expression-parse": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz",
- "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==",
- "dev": true,
- "dependencies": {
- "spdx-exceptions": "^2.1.0",
- "spdx-license-ids": "^3.0.0"
- }
- },
- "node_modules/spdx-license-ids": {
- "version": "3.0.13",
- "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz",
- "integrity": "sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==",
- "dev": true
- },
"node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
@@ -3462,17 +3331,6 @@
"to-fast-properties": "^2.0.0"
}
},
- "@es-joy/jsdoccomment": {
- "version": "0.37.0",
- "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.37.0.tgz",
- "integrity": "sha512-hjK0wnsPCYLlF+HHB4R/RbUjOWeLW2SlarB67+Do5WsKILOkmIZvvPJFbtWSmbypxcjpoECLAMzoao0D4Bg5ZQ==",
- "dev": true,
- "requires": {
- "comment-parser": "1.3.1",
- "esquery": "^1.4.0",
- "jsdoc-type-pratt-parser": "~4.0.0"
- }
- },
"@esbuild/android-arm": {
"version": "0.17.16",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.16.tgz",
@@ -3637,20 +3495,20 @@
}
},
"@eslint-community/regexpp": {
- "version": "4.5.0",
- "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.0.tgz",
- "integrity": "sha512-vITaYzIcNmjn5tF5uxcZ/ft7/RXGrMUIS9HalWckEOF6ESiwXKoMzAQf2UW0aVd6rnOeExTJVd5hmWXucBKGXQ==",
+ "version": "4.5.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.1.tgz",
+ "integrity": "sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==",
"dev": true
},
"@eslint/eslintrc": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.2.tgz",
- "integrity": "sha512-3W4f5tDUra+pA+FzgugqL2pRimUTDJWKr7BINqOpkZrC0uYI0NIc0/JFgBROCU07HR6GieA5m3/rsPIhDmCXTQ==",
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.3.tgz",
+ "integrity": "sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ==",
"dev": true,
"requires": {
"ajv": "^6.12.4",
"debug": "^4.3.2",
- "espree": "^9.5.1",
+ "espree": "^9.5.2",
"globals": "^13.19.0",
"ignore": "^5.2.0",
"import-fresh": "^3.2.1",
@@ -3671,9 +3529,9 @@
}
},
"@eslint/js": {
- "version": "8.38.0",
- "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.38.0.tgz",
- "integrity": "sha512-IoD2MfUnOV58ghIHCiil01PcohxjbYR/qCxsoC+xNgUwh1EY8jOOrYmu3d3a71+tJJ23uscEV4X2HJWMsPJu4g==",
+ "version": "8.40.0",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.40.0.tgz",
+ "integrity": "sha512-ElyB54bJIhXQYVKjDSvCkPO1iU1tSAeVQJbllWJq1XQSmmA4dgFk8CbiBGpiOPxleE48vDogxCtmMYku4HSVLA==",
"dev": true
},
"@humanwhocodes/config-array": {
@@ -4157,12 +4015,6 @@
"color-convert": "^1.9.0"
}
},
- "are-docs-informative": {
- "version": "0.0.2",
- "resolved": "https://registry.npmjs.org/are-docs-informative/-/are-docs-informative-0.0.2.tgz",
- "integrity": "sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==",
- "dev": true
- },
"argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@@ -4250,12 +4102,6 @@
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
"dev": true
},
- "comment-parser": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.3.1.tgz",
- "integrity": "sha512-B52sN2VNghyq5ofvUsqZjmk6YkihBX5vMSChmSK9v4ShjKf3Vk5Xcmgpw4o+iIgtrnM/u5FiMpz9VKb8lpBveA==",
- "dev": true
- },
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -4364,15 +4210,15 @@
"dev": true
},
"eslint": {
- "version": "8.38.0",
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.38.0.tgz",
- "integrity": "sha512-pIdsD2jwlUGf/U38Jv97t8lq6HpaU/G9NKbYmpWpZGw3LdTNhZLbJePqxOXGB5+JEKfOPU/XLxYxFh03nr1KTg==",
+ "version": "8.40.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.40.0.tgz",
+ "integrity": "sha512-bvR+TsP9EHL3TqNtj9sCNJVAFK3fBN8Q7g5waghxyRsPLIMwL73XSKnZFK0hk/O2ANC+iAoq6PWMQ+IfBAJIiQ==",
"dev": true,
"requires": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.4.0",
- "@eslint/eslintrc": "^2.0.2",
- "@eslint/js": "8.38.0",
+ "@eslint/eslintrc": "^2.0.3",
+ "@eslint/js": "8.40.0",
"@humanwhocodes/config-array": "^0.11.8",
"@humanwhocodes/module-importer": "^1.0.1",
"@nodelib/fs.walk": "^1.2.8",
@@ -4382,9 +4228,9 @@
"debug": "^4.3.2",
"doctrine": "^3.0.0",
"escape-string-regexp": "^4.0.0",
- "eslint-scope": "^7.1.1",
- "eslint-visitor-keys": "^3.4.0",
- "espree": "^9.5.1",
+ "eslint-scope": "^7.2.0",
+ "eslint-visitor-keys": "^3.4.1",
+ "espree": "^9.5.2",
"esquery": "^1.4.2",
"esutils": "^2.0.2",
"fast-deep-equal": "^3.1.3",
@@ -4477,54 +4323,6 @@
}
}
},
- "eslint-plugin-jsdoc": {
- "version": "41.1.1",
- "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-41.1.1.tgz",
- "integrity": "sha512-dfH97DKLGtQ5dgEMzd+GSUuY+xX/yyAfjML3O0pEWmMMpylsG6Ro65s4ziYXKmixiENYK9CTQxCVRGqZUFN2Mw==",
- "dev": true,
- "requires": {
- "@es-joy/jsdoccomment": "~0.37.0",
- "are-docs-informative": "^0.0.2",
- "comment-parser": "1.3.1",
- "debug": "^4.3.4",
- "escape-string-regexp": "^4.0.0",
- "esquery": "^1.5.0",
- "semver": "^7.3.8",
- "spdx-expression-parse": "^3.0.1"
- },
- "dependencies": {
- "escape-string-regexp": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
- "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
- "dev": true
- },
- "lru-cache": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
- "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
- "dev": true,
- "requires": {
- "yallist": "^4.0.0"
- }
- },
- "semver": {
- "version": "7.4.0",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.4.0.tgz",
- "integrity": "sha512-RgOxM8Mw+7Zus0+zcLEUn8+JfoLpj/huFTItQy2hsM4khuC1HYRDp0cU482Ewn/Fcy6bCjufD8vAj7voC66KQw==",
- "dev": true,
- "requires": {
- "lru-cache": "^6.0.0"
- }
- },
- "yallist": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
- "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
- "dev": true
- }
- }
- },
"eslint-scope": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz",
@@ -4536,20 +4334,20 @@
}
},
"eslint-visitor-keys": {
- "version": "3.4.0",
- "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.0.tgz",
- "integrity": "sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ==",
+ "version": "3.4.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz",
+ "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==",
"dev": true
},
"espree": {
- "version": "9.5.1",
- "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.1.tgz",
- "integrity": "sha512-5yxtHSZXRSW5pvv3hAlXM5+/Oswi1AUFqBmbibKb5s6bp3rGIDkyXU6xCoyuuLhijr4SFwPrXRoZjz0AZDN9tg==",
+ "version": "9.5.2",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.2.tgz",
+ "integrity": "sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw==",
"dev": true,
"requires": {
"acorn": "^8.8.0",
"acorn-jsx": "^5.3.2",
- "eslint-visitor-keys": "^3.4.0"
+ "eslint-visitor-keys": "^3.4.1"
}
},
"esquery": {
@@ -4835,12 +4633,6 @@
"argparse": "^2.0.1"
}
},
- "jsdoc-type-pratt-parser": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.0.0.tgz",
- "integrity": "sha512-YtOli5Cmzy3q4dP26GraSOeAhqecewG04hoO8DY56CH4KJ9Fvv5qKWUCCo3HZob7esJQHCv6/+bnTy72xZZaVQ==",
- "dev": true
- },
"jsesc": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
@@ -5230,28 +5022,6 @@
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
"dev": true
},
- "spdx-exceptions": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz",
- "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==",
- "dev": true
- },
- "spdx-expression-parse": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz",
- "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==",
- "dev": true,
- "requires": {
- "spdx-exceptions": "^2.1.0",
- "spdx-license-ids": "^3.0.0"
- }
- },
- "spdx-license-ids": {
- "version": "3.0.13",
- "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz",
- "integrity": "sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==",
- "dev": true
- },
"strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
diff --git a/package.json b/package.json
index 0b8b111..d6cec7a 100644
--- a/package.json
+++ b/package.json
@@ -6,7 +6,8 @@
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
- "lint": "prettier --ignore-path .gitignore --check ."
+ "lint": "prettier --ignore-path .gitignore --check . && eslint --ignore-path .gitignore .",
+ "fix:lint": "prettier --ignore-path .gitignore --write . && eslint --ignore-path .gitignore --fix ."
},
"dependencies": {
"@jupyter-widgets/base": "^6.0.4",
@@ -17,6 +18,7 @@
"@preact/preset-vite": "^2.5.0",
"@types/react": "<18",
"@types/react-dom": "<18",
+ "eslint": "^8.40.0",
"prettier": "^2.8.7",
"typescript": "^5.0.2",
"vite": "^4.2.1"
diff --git a/reactpy_jupyter.pth b/reactpy_jupyter.pth
new file mode 100644
index 0000000..79174b8
--- /dev/null
+++ b/reactpy_jupyter.pth
@@ -0,0 +1 @@
+import reactpy_jupyter
diff --git a/reactpy_jupyter/__init__.py b/reactpy_jupyter/__init__.py
index f8ca9fa..81424e3 100644
--- a/reactpy_jupyter/__init__.py
+++ b/reactpy_jupyter/__init__.py
@@ -6,20 +6,22 @@
from . import jupyter_server_extension
from .import_resources import setup_import_resources
-from .ipython_extension import load_ipython_extension, unload_ipython_extension
-from .widget import LayoutWidget, run, set_import_source_base_url, widgetize
+from .layout_widget import run, set_import_source_base_url, to_widget
+from .monkey_patch import execute_patch
+from .widget_component import from_widget
__version__ = "0.8.1" # DO NOT MODIFY
-__all__ = [
- "LayoutWidget",
- "widgetize",
- "run",
+__all__ = (
+ "from_widget",
"load_ipython_extension",
"unload_ipython_extension",
+ "to_widget",
+ "run",
"set_import_source_base_url",
"jupyter_server_extension",
-]
+)
setup_import_resources()
+execute_patch()
diff --git a/reactpy_jupyter/import_resources.py b/reactpy_jupyter/import_resources.py
index 0a0a91d..e57d0bb 100644
--- a/reactpy_jupyter/import_resources.py
+++ b/reactpy_jupyter/import_resources.py
@@ -14,7 +14,7 @@
REACTPY_RESOURCE_BASE_PATH,
REACTPY_WEB_MODULES_DIR,
)
-from .widget import set_import_source_base_url
+from .layout_widget import set_import_source_base_url
logger = logging.getLogger(__name__)
diff --git a/reactpy_jupyter/ipython_extension.py b/reactpy_jupyter/ipython_extension.py
deleted file mode 100644
index c2a35ed..0000000
--- a/reactpy_jupyter/ipython_extension.py
+++ /dev/null
@@ -1,40 +0,0 @@
-from __future__ import annotations
-
-from functools import partial
-
-from IPython import get_ipython
-from IPython.core.interactiveshell import ExecutionResult, InteractiveShell
-from IPython.display import display
-from reactpy.core.component import ComponentType
-
-from .widget import LayoutWidget
-
-_EXTENSION_LOADED = False
-_POST_RUN_CELL_HOOK = None
-
-
-def load_ipython_extension(ipython: InteractiveShell) -> None:
- global _POST_RUN_CELL_HOOK, _EXTENSION_LOADED
- if not _EXTENSION_LOADED:
- _POST_RUN_CELL_HOOK = partial(_post_run_cell, ipython)
- ipython.events.register("post_run_cell", _POST_RUN_CELL_HOOK)
- ipython.display_formatter.ipython_display_formatter.for_type(
- ComponentType, lambda component: ({}, {})
- )
- _EXTENSION_LOADED = True
-
-
-def unload_ipython_extension(ipython: InteractiveShell) -> None:
- global _POST_RUN_CELL_HOOK, _EXTENSION_LOADED
- ipython.events.unregister("post_run_cell", _POST_RUN_CELL_HOOK)
- _POST_RUN_CELL_HOOK = None
- _EXTENSION_LOADED = False
-
-
-def _post_run_cell(ipython: InteractiveShell, result: ExecutionResult) -> None:
- if isinstance(result.result, ComponentType):
- display(LayoutWidget(result.result))
-
-
-if get_ipython() is not None:
- load_ipython_extension(get_ipython())
diff --git a/reactpy_jupyter/widget.py b/reactpy_jupyter/layout_widget.py
similarity index 67%
rename from reactpy_jupyter/widget.py
rename to reactpy_jupyter/layout_widget.py
index 397d055..4a7de1a 100644
--- a/reactpy_jupyter/widget.py
+++ b/reactpy_jupyter/layout_widget.py
@@ -1,31 +1,27 @@
from __future__ import annotations
import asyncio
-import os
from functools import wraps
from pathlib import Path
from queue import Queue as SyncQueue
from threading import Thread
-from typing import Any, Awaitable, Callable
+from typing import Any, Awaitable, Callable, overload
import anywidget
from IPython.display import DisplayHandle
from IPython.display import display as ipython_display
+from ipywidgets import Widget, widget_serialization
from jsonpointer import set_pointer
from reactpy.core.layout import Layout
from reactpy.core.types import ComponentType
-from traitlets import Unicode
+from traitlets import Instance, List, Unicode
from typing_extensions import ParamSpec
-DEV = bool(int(os.environ.get("REACTPY_JUPYTER_DEV", "0")))
+from reactpy_jupyter.widget_component import InnerWidgets, inner_widgets_context
-if DEV:
- # from `npx vite`
- ESM = "http://localhost:5173/src/index.js?anywidget"
-else:
- # from `npx vite build`
- bundled_assets_dir = Path(__file__).parent / "static"
- ESM = (bundled_assets_dir / "index.js").read_text()
+# from `npx vite build`
+bundled_assets_dir = Path(__file__).parent / "static"
+ESM = (bundled_assets_dir / "index.js").read_text()
def set_import_source_base_url(base_url: str) -> None:
@@ -45,12 +41,27 @@ def run(constructor: Callable[[], ComponentType]) -> DisplayHandle | None:
_P = ParamSpec("_P")
-def widgetize(constructor: Callable[_P, ComponentType]) -> Callable[_P, LayoutWidget]:
- """A decorator that turns an ReactPy element into a Jupyter Widget constructor"""
+@overload
+def to_widget(value: Callable[_P, ComponentType]) -> Callable[_P, LayoutWidget]:
+ ...
- @wraps(constructor)
+
+@overload
+def to_widget(value: ComponentType) -> LayoutWidget:
+ ...
+
+
+def to_widget(
+ value: Callable[_P, ComponentType] | ComponentType
+) -> Callable[_P, LayoutWidget] | LayoutWidget:
+ """Turn a component into a widget or a component construtor into a widget constructor"""
+
+ if isinstance(value, ComponentType):
+ return LayoutWidget(value)
+
+ @wraps(value)
def wrapper(*args: Any, **kwargs: Any) -> LayoutWidget:
- return LayoutWidget(constructor(*args, **kwargs))
+ return LayoutWidget(value(*args, **kwargs))
return wrapper
@@ -60,12 +71,21 @@ class LayoutWidget(anywidget.AnyWidget):
_esm = ESM
_import_source_base_url = Unicode().tag(sync=True)
+ _inner_widgets = List(Instance(Widget)).tag(sync=True, **widget_serialization)
def __init__(self, component: ComponentType) -> None:
- super().__init__(_import_source_base_url=_IMPORT_SOURCE_BASE_URL)
+ super().__init__(
+ _import_source_base_url=_IMPORT_SOURCE_BASE_URL,
+ _inner_widgets=[],
+ )
self._reactpy_model = {}
self._reactpy_views = set()
- self._reactpy_layout = Layout(component)
+ self._reactpy_layout = Layout(
+ inner_widgets_context(
+ component,
+ value=InnerWidgets(self._add_inner_widget, self._remove_inner_widget),
+ )
+ )
self._reactpy_loop = _spawn_threaded_event_loop(
self._reactpy_layout_render_loop()
)
@@ -107,9 +127,20 @@ async def _reactpy_layout_render_loop(self) -> None:
for v_id in self._reactpy_views:
self.send({"viewID": v_id, "data": update_message})
+ def _add_inner_widget(self, widget: Widget) -> None:
+ self._inner_widgets = self._inner_widgets + [widget]
+
+ def _remove_inner_widget(self, widget: Widget) -> None:
+ self._inner_widgets = [w for w in self._inner_widgets if w != widget]
+
def __repr__(self) -> str:
return f"LayoutWidget({self._reactpy_layout})"
+ @classmethod
+ def _dev(cls) -> None:
+ """Load the widget from the dev server"""
+ cls._esm = "http://localhost:5173/src/index.js"
+
def _spawn_threaded_event_loop(
coro: Callable[..., Awaitable[Any]]
diff --git a/reactpy_jupyter/monkey_patch.py b/reactpy_jupyter/monkey_patch.py
new file mode 100644
index 0000000..a60452e
--- /dev/null
+++ b/reactpy_jupyter/monkey_patch.py
@@ -0,0 +1,25 @@
+from typing import Any
+from weakref import finalize
+
+from reactpy.core.component import Component
+
+from reactpy_jupyter.layout_widget import to_widget
+
+# we can't track the widgets by adding them as a hidden attribute to the component
+# because Component has __slots__ defined
+LIVE_WIDGETS: dict[int, Any] = {}
+
+
+def execute_patch() -> None:
+ """Monkey patch ReactPy's Component class to display as a Jupyter widget"""
+
+ def _repr_mimebundle_(self: Component, *a, **kw) -> None:
+ self_id = id(self)
+ if self_id not in LIVE_WIDGETS:
+ widget = LIVE_WIDGETS[self_id] = to_widget(self)
+ finalize(self, lambda: LIVE_WIDGETS.pop(self_id, None))
+ else:
+ widget = LIVE_WIDGETS[self_id]
+ return widget._repr_mimebundle_(*a, **kw)
+
+ Component._repr_mimebundle_ = _repr_mimebundle_
diff --git a/reactpy_jupyter/widget_component.py b/reactpy_jupyter/widget_component.py
new file mode 100644
index 0000000..fd96971
--- /dev/null
+++ b/reactpy_jupyter/widget_component.py
@@ -0,0 +1,31 @@
+from __future__ import annotations
+
+from typing import Callable
+
+from attr import dataclass
+from ipywidgets import Widget
+from reactpy import component, create_context, html, use_context, use_effect
+from reactpy.types import Context, VdomDict
+
+inner_widgets_context: Context[InnerWidgets | None] = create_context(None)
+
+
+@component
+def from_widget(source: Widget) -> VdomDict:
+ inner_widgets = use_context(inner_widgets_context)
+
+ @use_effect
+ def add_widget():
+ inner_widgets.add(source)
+ return lambda: inner_widgets.remove(source)
+
+ if inner_widgets is None:
+ raise RuntimeError("Jupyter component must be rendered inside a JupyterLayout")
+
+ return html.span({"class": f"widget-model-id-{source.model_id}"})
+
+
+@dataclass
+class InnerWidgets:
+ add: Callable[[Widget], None]
+ remove: Callable[[Widget], None]
diff --git a/requirements.txt b/requirements.txt
index 21ada24..bdd7471 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -2,3 +2,5 @@ twine
jupyter-packaging
jupyter-repo2docker
noxopt
+jupyterlab
+
diff --git a/setup.py b/setup.py
index 9271ee7..3aa16b8 100644
--- a/setup.py
+++ b/setup.py
@@ -5,8 +5,11 @@
import subprocess
import sys
import traceback
+from distutils.cmd import Command
+from functools import partial
from logging import StreamHandler, getLogger
from pathlib import Path
+from typing import Callable
from jupyter_packaging import get_data_files
from setuptools import find_packages, setup
@@ -136,46 +139,68 @@ def list2cmdline(cmd_list):
)
# --------------------------------------------------------------------------------------
-# Build Javascript
+# Command Classes
# --------------------------------------------------------------------------------------
-def build_javascript_first(cls):
+def build_javascript_first(cmd: Command):
+ log.info("Installing Javascript...")
+ try:
+ npm = shutil.which("npm") # this is required on windows
+ if npm is None:
+ raise RuntimeError("NPM is not installed.")
+ for args in (f"{npm} ci", f"{npm} run build"):
+ args_list = args.split()
+ log.info(f"> {list2cmdline(args_list)}")
+ subprocess.run(args_list, cwd=str(ROOT_DIR), check=True)
+ except Exception:
+ log.error("Failed to install Javascript")
+ log.error(traceback.format_exc())
+ raise
+ else:
+ log.info("Successfully installed Javascript")
+
+
+def build_with_pth_file(cmd: Command):
+ # install the pth file
+ pth_filename = f"{NAME}.pth"
+ source_pth_file = ROOT_DIR / pth_filename
+ source_pth_file = Path(cmd.build_lib, pth_filename)
+ cmd.copy_file(str(source_pth_file), str(source_pth_file))
+
+
+def add_to_cmd(cls: Command, functions: list[Callable[[Command], None]]) -> Command:
class Command(cls):
def run(self):
- log.info("Installing Javascript...")
- try:
- npm = shutil.which("npm") # this is required on windows
- if npm is None:
- raise RuntimeError("NPM is not installed.")
- for args in (f"{npm} ci", f"{npm} run build"):
- args_list = args.split()
- log.info(f"> {list2cmdline(args_list)}")
- subprocess.run(args_list, cwd=str(ROOT_DIR), check=True)
- except Exception:
- log.error("Failed to install Javascript")
- log.error(traceback.format_exc())
- raise
- else:
- log.info("Successfully installed Javascript")
super().run()
+ for f in functions:
+ f(self)
return Command
+cmd_additions = partial(
+ add_to_cmd,
+ functions=[
+ build_javascript_first,
+ build_with_pth_file,
+ ],
+)
+
+
package["cmdclass"] = {
- "sdist": build_javascript_first(sdist),
- "develop": build_javascript_first(develop),
+ "sdist": cmd_additions(sdist),
+ "develop": cmd_additions(develop),
}
if sys.version_info < (3, 10, 6):
from distutils.command.build import build
- package["cmdclass"]["build"] = build_javascript_first(build)
+ package["cmdclass"]["build"] = cmd_additions(build)
else:
from setuptools.command.build_py import build_py
- package["cmdclass"]["build_py"] = build_javascript_first(build_py)
+ package["cmdclass"]["build_py"] = cmd_additions(build_py)
# --------------------------------------------------------------------------------------
diff --git a/src/index.js b/src/index.js
index d69e189..3af685b 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,10 +1,33 @@
+/**
+ * @typedef {import("@jupyter-widgets/base").DOMWidgetView} DOMWidgetView
+ */
import { BaseReactPyClient, mount } from "@reactpy/client";
-import { DOMWidgetView } from "@jupyter-widgets/base";
/**@param view {DOMWidgetView} view */
export function render(view) {
const client = new JupyterReactPyClient(view);
mount(view.el, client);
+
+ async function updateInnerWidgets() {
+ /** @type {String[]} */
+ let innerModelIds = view.model.get("_inner_widgets");
+
+ for (let modelId of innerModelIds.map((id) =>
+ id.slice("IPY_MODEL_".length)
+ )) {
+ let model = await view.model.widget_manager.get_model(modelId);
+ (await waitForSelectorAll(`.widget-model-id-${modelId}`, view.el)).map(
+ async (containerEl) => {
+ let childView = await view.create_child_view(model);
+ containerEl.replaceChildren(childView.el);
+ }
+ );
+ }
+ }
+
+ view.model.on("change:_inner_widgets", updateInnerWidgets);
+
+ updateInnerWidgets();
}
let viewID = 0;
@@ -12,9 +35,8 @@ let viewID = 0;
class JupyterReactPyClient extends BaseReactPyClient {
/**
* @param view {DOMWidgetView}
- * @param viewID {number}
*/
- constructor(view, viewId) {
+ constructor(view) {
super();
this.view = view;
this.viewID = viewID++;
@@ -86,10 +108,10 @@ const jupyterServerBaseUrl = (() => {
})();
function concatAndResolveUrl(url, concat) {
- var url1 = (url.endsWith("/") ? url.slice(0, -1) : url).split("/");
- var url2 = concat.split("/");
- var url3 = [];
- for (var i = 0, l = url1.length; i < l; i++) {
+ let url1 = (url.endsWith("/") ? url.slice(0, -1) : url).split("/");
+ let url2 = concat.split("/");
+ let url3 = [];
+ for (let i = 0, l = url1.length; i < l; i++) {
if (url1[i] == "..") {
url3.pop();
} else if (url1[i] == ".") {
@@ -98,7 +120,7 @@ function concatAndResolveUrl(url, concat) {
url3.push(url1[i]);
}
}
- for (var i = 0, l = url2.length; i < l; i++) {
+ for (let i = 0, l = url2.length; i < l; i++) {
if (url2[i] == "..") {
url3.pop();
} else if (url2[i] == ".") {
@@ -109,3 +131,34 @@ function concatAndResolveUrl(url, concat) {
}
return url3.join("/");
}
+
+/**
+ * @param {String} selector
+ * @param {HTMLElement} containerElement
+ * @returns {Promise}
+ */
+function waitForSelectorAll(selector, containerElement) {
+ return new Promise((resolve) => {
+ const resolveSearch = () => {
+ const elements = Array.from(document.querySelectorAll(selector));
+ if (elements.length) {
+ resolve(elements);
+ return true;
+ }
+ return false;
+ };
+
+ if (resolveSearch()) {
+ return;
+ }
+
+ const observer = new MutationObserver(() =>
+ resolveSearch() ? observer.disconnect() : null
+ );
+
+ observer.observe(containerElement, {
+ childList: true,
+ subtree: true,
+ });
+ });
+}