diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml index 2b1be72..ffd3342 100644 --- a/.github/workflows/unit.yml +++ b/.github/workflows/unit.yml @@ -9,13 +9,19 @@ on: pull_request: workflow_dispatch: +env: + PKG_URL: https://nbg1.your-objectstorage.com/ipyvue3-packages/packages + +permissions: + contents: read + jobs: code-quality: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Install Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: "3.11" - name: Install dependencies @@ -28,23 +34,23 @@ jobs: build: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Install node - uses: actions/setup-node@v1 + uses: actions/setup-node@v4 with: - node-version: "14.x" + node-version: "20.x" registry-url: "https://registry.npmjs.org" - name: Install Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: "3.11" - name: Install dependencies run: | python -m pip install --upgrade pip - pip install twine wheel jupyter-packaging "jupyterlab<4" + pip install twine wheel jupyter-packaging "jupyterlab<5" - name: Build run: | @@ -70,22 +76,43 @@ jobs: ./dist ./js/*.tgz + - name: Publish dev wheel to object storage + if: > + github.event_name == 'pull_request' && + github.event.pull_request.head.ref == 'vue3' && + github.event.pull_request.head.repo.full_name == github.repository + env: + AWS_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET_ACCESS_KEY }} + AWS_REGION: nbg1 + AWS_ENDPOINT_URL_S3: https://nbg1.your-objectstorage.com + S3_BUCKET: ipyvue3-packages + run: | + python -m pip install --upgrade awscli + + aws s3 cp dist/ "s3://${S3_BUCKET}/packages/ipyvue/" \ + --recursive \ + --exclude "*" \ + --include "*.whl" \ + --endpoint-url "$AWS_ENDPOINT_URL_S3" \ + --region "$AWS_REGION" + test: needs: [build] runs-on: ubuntu-24.04 strategy: fail-fast: false matrix: - python-version: [3.8, 3.9, "3.10", "3.11"] + python-version: [3.8, 3.9, "3.10", "3.11", "3.12"] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - uses: actions/download-artifact@v4 with: name: ipyvue-dist-${{ github.run_number }} - name: Install Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} @@ -107,21 +134,26 @@ jobs: needs: [build] runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - uses: actions/download-artifact@v4 with: name: ipyvue-dist-${{ github.run_number }} - name: Install Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: 3.8 - name: Install vuetify and test deps run: | wheel=(dist/*.whl) - pip install ${wheel}[test] "jupyter_server<2" + pip install ${wheel} + pip install "${PKG_URL}/ipyvuetify/ipyvuetify-3.0.0a3-py2.py3-none-any.whl" + pip install "solara-ui[all] @ ${PKG_URL}/solara/solara_ui-1.57.3-py3-none-any.whl" + pip install "solara-server[starlette,dev] @ ${PKG_URL}/solara-server/solara_server-1.57.3-py3-none-any.whl" + pip install "pytest-ipywidgets[all] @ ${PKG_URL}/pytest-ipywidgets/pytest_ipywidgets-1.57.3-py3-none-any.whl" + pip install "jupyter_server<2" - name: Install playwright browsers run: playwright install chromium @@ -137,7 +169,7 @@ jobs: path: test-results release-dry-run: - needs: [ test,ui-test,code-quality ] + needs: [test, ui-test, code-quality] runs-on: ubuntu-24.04 steps: - uses: actions/download-artifact@v4 @@ -145,9 +177,9 @@ jobs: name: ipyvue-dist-${{ github.run_number }} - name: Install node - uses: actions/setup-node@v1 + uses: actions/setup-node@v4 with: - node-version: "14.x" + node-version: "20.x" registry-url: "https://registry.npmjs.org" - name: Publish the NPM package @@ -159,6 +191,7 @@ jobs: env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} PRE_RELEASE: ${{ github.event.release.prerelease }} + release: if: startsWith(github.event.ref, 'refs/tags/v') needs: [release-dry-run] @@ -169,15 +202,15 @@ jobs: name: ipyvue-dist-${{ github.run_number }} - name: Install node - uses: actions/setup-node@v1 + uses: actions/setup-node@v4 with: - node-version: "14.x" + node-version: "20.x" registry-url: "https://registry.npmjs.org" - name: Install Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: - python-version: 3.8 + python-version: "3.11" - name: Install dependencies run: | diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7555f0f..169753a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,6 +4,6 @@ repos: hooks: - id: black - repo: https://github.com/PyCQA/flake8 - rev: 3.9.2 + rev: 7.1.1 hooks: - id: flake8 diff --git a/README.md b/README.md index efa6dae..ae1c4db 100755 --- a/README.md +++ b/README.md @@ -29,21 +29,8 @@ For a development installation (requires npm), Scoped CSS Support ------------------ -` """ -widget = MyComponent(scoped_css_support=True) -``` - -Note: The `css` trait with `scoped=True` always works, regardless of this setting: - -```python -widget = VueTemplate( - template="", - css=".x { color: blue; }", - scoped=True -) +widget = MyComponent() ``` Sponsors diff --git a/class_components.ipynb b/class_components.ipynb index 99893b3..2a65f2c 100644 --- a/class_components.ipynb +++ b/class_components.ipynb @@ -21,9 +21,11 @@ " input = Unicode().tag(sync=True)\n", " something = Unicode('defaultvalue').tag(sync=True)\n", " template = Unicode('''\n", + " \n", " ''').tag(sync=True)\n", " \n", " def vue_append_one(self, *args):\n", @@ -44,6 +46,7 @@ " texts = List(['xxxx', 'yyyy']).tag(sync=True)\n", " direct = Unicode('aaa').tag(sync=True)\n", " template = Unicode('''\n", + " \n", " ''').tag(sync=True)\n", " \n", " components=Dict({\n", @@ -113,9 +117,11 @@ " label = Unicode().tag(sync=True)\n", " \n", " template = Unicode('''\n", + " \n", " ''').tag(sync=True)\n", " \n", " @observe('db')\n", @@ -135,6 +141,7 @@ " supplier_db_collection = Dict().tag(sync_ref=True)\n", " \n", " template = Unicode('''\n", + " \n", " ''').tag(sync=True)\n", " \n", " components = Dict({\n", diff --git a/examples/DeepWatch.ipynb b/examples/DeepWatch.ipynb index 7fcc66a..f090e7c 100644 --- a/examples/DeepWatch.ipynb +++ b/examples/DeepWatch.ipynb @@ -15,6 +15,7 @@ " deep_array = traitlets.List(['array']).tag(sync=True)\n", " deep_array2 = traitlets.List([{'prop': 'array2'}]).tag(sync=True)\n", " template = traitlets.Unicode('''\n", + " \n", " ''').tag(sync=True)\n", " \n", "md = MyDeep()\n", diff --git a/examples/EmbeddingJupyterWidgetsInVueTemplate.ipynb b/examples/EmbeddingJupyterWidgetsInVueTemplate.ipynb index fd03f42..1d4f73f 100644 --- a/examples/EmbeddingJupyterWidgetsInVueTemplate.ipynb +++ b/examples/EmbeddingJupyterWidgetsInVueTemplate.ipynb @@ -29,12 +29,14 @@ "\n", "\n", " template=Unicode(\"\"\"\n", - "
\n", - "
\n", - " {{ item.title }}: \n", + " \n", " \"\"\").tag(sync=True)\n", "\n", "my_component = MyComponent()\n", diff --git a/examples/ScopedCSS.ipynb b/examples/ScopedCSS.ipynb index 3f04f0e..3f4ec3b 100644 --- a/examples/ScopedCSS.ipynb +++ b/examples/ScopedCSS.ipynb @@ -6,9 +6,9 @@ "source": [ "# Scoped CSS\n", "\n", - "By default, CSS in ipyvue templates is **global** — it affects all elements on the page with matching selectors. Scoped CSS limits styles to the component that defines them.\n", + "CSS in ipyvue templates is global unless it is marked with ` + """ + + +def test_template_style_update_replaces_old_styles( + solara_test, page_session: playwright.sync_api.Page +): + widget = StyledTemplate() + + display(widget) + + target = page_session.locator(".style-leak-target") + target.wait_for() + expect_red = """ + () => { + const el = document.querySelector('.style-leak-target'); + return el && getComputedStyle(el).color === 'rgb(255, 0, 0)'; + } + """ + page_session.wait_for_function(expect_red) + + def update_template(): + widget.template = """ + + """ + + threading.Timer(0.5, update_template).start() + + page_session.locator("text=Unstyled").wait_for() + page_session.wait_for_function( + """ + () => { + const el = document.querySelector('.style-leak-target'); + return el && getComputedStyle(el).color !== 'rgb(255, 0, 0)'; + } + """ + ) + + class MyTemplateScript(vue.VueTemplate): clicks = Int(0).tag(sync=True) @@ -124,8 +208,6 @@ def test_template_script( ipywidgets_runner, page_session: playwright.sync_api.Page, template_class_name ): def kernel_code(template_class_name=template_class_name): - # this import is need so when this code executes in the kernel, - # the class is imported from test_template import MyTemplateScript, MyTemplateScriptOld template_class = { @@ -167,7 +249,7 @@ def kernel_code(): import ipywidgets as widgets from IPython.display import display - scoped = ScopedStyleTemplate(scoped_css_support=True) + scoped = ScopedStyleTemplate() unscoped = vue.Html( tag="span", children=["Unscoped text"], @@ -187,46 +269,3 @@ def kernel_code(): ) assert scoped_color == "rgb(255, 0, 0)" assert unscoped_color != "rgb(255, 0, 0)" - - -class ScopedCssTemplate(vue.VueTemplate): - @default("template") - def _default_vue_template(self): - return """ - - """ - - -def test_template_scoped_css_trait( - ipywidgets_runner, page_session: playwright.sync_api.Page -): - def kernel_code(): - from test_template import ScopedCssTemplate - import ipyvue as vue - import ipywidgets as widgets - from IPython.display import display - - scoped = ScopedCssTemplate( - css=".scoped-css-text { color: rgb(0, 128, 0); }", scoped=True - ) - unscoped = vue.Html( - tag="span", - children=["Unscoped css text"], - class_="scoped-css-text", - attributes={"id": "unscoped-css-text"}, - ) - display(widgets.VBox([scoped, unscoped])) - - ipywidgets_runner(kernel_code) - page_session.locator("#scoped-css-text").wait_for() - page_session.locator("#unscoped-css-text").wait_for() - scoped_color = page_session.eval_on_selector( - "#scoped-css-text", "el => getComputedStyle(el).color" - ) - unscoped_color = page_session.eval_on_selector( - "#unscoped-css-text", "el => getComputedStyle(el).color" - ) - assert scoped_color == "rgb(0, 128, 0)" - assert unscoped_color != "rgb(0, 128, 0)" diff --git a/tests/ui/test_v_bind.py b/tests/ui/test_v_bind.py new file mode 100644 index 0000000..779f60e --- /dev/null +++ b/tests/ui/test_v_bind.py @@ -0,0 +1,41 @@ +import pytest +import sys + +if sys.version_info < (3, 7): + pytest.skip("requires python3.7 or higher", allow_module_level=True) + +import playwright.sync_api + +from IPython.display import display + + +@pytest.mark.parametrize("ipywidgets_runner", ["solara"], indirect=True) +def test_v_bind_supports_vuetify3_activator_props( + ipywidgets_runner, + page_session: playwright.sync_api.Page, +): + def kernel_code(): + import ipyvuetify as v + + tooltip = v.Tooltip( + location="bottom", + v_slots=[ + { + "name": "activator", + "variable": "tooltip", + "children": v.Btn( + v_bind="tooltip.props", + children=["Hover me"], + class_="v-bind-tooltip-button", + ), + } + ], + children=["Tooltip via v_bind"], + ) + + display(tooltip) + + ipywidgets_runner(kernel_code) + page_session.locator(".v-bind-tooltip-button").wait_for() + page_session.locator(".v-bind-tooltip-button").hover() + page_session.get_by_text("Tooltip via v_bind", exact=True).wait_for() diff --git a/tests/ui/test_v_on.py b/tests/ui/test_v_on.py new file mode 100644 index 0000000..0479517 --- /dev/null +++ b/tests/ui/test_v_on.py @@ -0,0 +1,59 @@ +import pytest +import sys + +if sys.version_info < (3, 7): + pytest.skip("requires python3.7 or higher", allow_module_level=True) + +import playwright.sync_api + + +@pytest.mark.parametrize("ipywidgets_runner", ["solara"], indirect=True) +def test_v_on_supports_nested_slot_scope_paths_and_vuetify3_props_fallback( + ipywidgets_runner, + page_session: playwright.sync_api.Page, +): + ipywidgets_runner(lambda: None) + page_session.wait_for_function("window.requirejs !== undefined") + + resolved = page_session.evaluate( + """() => new Promise((resolve, reject) => { + requirejs(["jupyter-vue"], (jupyterVue) => { + try { + const nested = jupyterVue.getScope("scopeData.nested", { + scopeData: { + nested: { + onClick: "nested-click", + onMouseenter: "nested-hover", + }, + }, + }); + const tooltip = jupyterVue.getScope("tooltip.on", { + tooltip: { + props: { + onMouseenter: "tooltip-hover", + id: "ignored-non-event-prop", + }, + }, + }); + + resolve({ + nestedKeys: Object.keys(nested || {}).sort(), + nestedClick: nested && nested.onClick, + tooltipKeys: Object.keys(tooltip || {}).sort(), + tooltipHover: tooltip && tooltip.onMouseenter, + tooltipId: tooltip && tooltip.id, + }); + } catch (error) { + reject(error); + } + }, reject); + })""" + ) + + assert resolved == { + "nestedKeys": ["onClick", "onMouseenter"], + "nestedClick": "nested-click", + "tooltipKeys": ["onMouseenter"], + "tooltipHover": "tooltip-hover", + "tooltipId": None, + } diff --git a/tests/unit/test_vue_widget.py b/tests/unit/test_vue_widget.py index 9e3b52c..35bc963 100644 --- a/tests/unit/test_vue_widget.py +++ b/tests/unit/test_vue_widget.py @@ -77,3 +77,11 @@ def test_event_handling(): event_handler.reset_mock() button._handle_event(None, dict(event="click.stop", data={"foo": "bar"}), []) event_handler.assert_called_once_with(button, "click.stop", {"foo": "bar"}) + + event_handler.reset_mock() + button._handle_event(None, dict(event="click.stop", data=0), []) + event_handler.assert_called_once_with(button, "click.stop", 0) + + event_handler.reset_mock() + button.click(False) + event_handler.assert_called_once_with(button, "click.stop", False)