Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-ipyvue for openSUSE:Factory checked in at 2026-03-16 14:16:56 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-ipyvue (Old) and /work/SRC/openSUSE:Factory/.python-ipyvue.new.8177 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-ipyvue" Mon Mar 16 14:16:56 2026 rev:11 rq:1339143 version:1.12.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-ipyvue/python-ipyvue.changes 2024-09-16 17:43:56.499204592 +0200 +++ /work/SRC/openSUSE:Factory/.python-ipyvue.new.8177/python-ipyvue.changes 2026-03-16 14:20:06.664214579 +0100 @@ -1,0 +2,14 @@ +Sun Mar 15 19:07:43 UTC 2026 - Dirk Müller <[email protected]> + +- update to 1.12.0: + * fix: add a default sourceURL to aid debugging + * feat: add scoped CSS support + * fix: make scoped CSS support optional via opt-in + +------------------------------------------------------------------- +Mon Sep 29 11:20:29 UTC 2025 - Dirk Müller <[email protected]> + +- update to 1.11.3: + * fix: fix: a data value of `0` turns into `{}` + +------------------------------------------------------------------- Old: ---- ipyvue-1.11.1-gh.tar.gz ipyvue-1.11.1.tar.gz New: ---- ipyvue-1.12.0-gh.tar.gz ipyvue-1.12.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-ipyvue.spec ++++++ --- /var/tmp/diff_new_pack.9f2jYH/_old 2026-03-16 14:20:07.332242310 +0100 +++ /var/tmp/diff_new_pack.9f2jYH/_new 2026-03-16 14:20:07.336242475 +0100 @@ -1,7 +1,7 @@ # # spec file for package python-ipyvue # -# Copyright (c) 2024 SUSE LLC +# Copyright (c) 2026 SUSE LLC and contributors # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -17,9 +17,9 @@ # This is important for versions ending in .0 -%define python3dist_version 1.11.1 +%define python3dist_version 1.12 Name: python-ipyvue -Version: 1.11.1 +Version: 1.12.0 Release: 0 Summary: Jupyter widgets base for Vue libraries License: MIT ++++++ ipyvue-1.11.1-gh.tar.gz -> ipyvue-1.12.0-gh.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ipyvue-1.11.1/.bumpversion.cfg new/ipyvue-1.12.0/.bumpversion.cfg --- old/ipyvue-1.11.1/.bumpversion.cfg 2024-05-02 10:02:04.000000000 +0200 +++ new/ipyvue-1.12.0/.bumpversion.cfg 2026-02-11 11:02:04.000000000 +0100 @@ -1,5 +1,5 @@ [bumpversion] -current_version = 1.11.1 +current_version = 1.12.0 commit = True message = chore: bump version: {current_version} → {new_version} tag = True diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ipyvue-1.11.1/.github/workflows/unit.yml new/ipyvue-1.12.0/.github/workflows/unit.yml --- old/ipyvue-1.11.1/.github/workflows/unit.yml 2024-05-02 10:02:04.000000000 +0200 +++ new/ipyvue-1.12.0/.github/workflows/unit.yml 2026-02-11 11:02:04.000000000 +0100 @@ -11,7 +11,7 @@ jobs: code-quality: - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v2 - name: Install Python @@ -26,7 +26,7 @@ uses: rbialon/flake8-annotations@v1 build: - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v2 @@ -63,7 +63,7 @@ if [[ $(ls -1 count | wc -l) -ne 3 ]]; then echo "Expected 4 files/directory"; exit 1; fi - name: Upload builds - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: ipyvue-dist-${{ github.run_number }} path: | @@ -72,15 +72,15 @@ test: needs: [build] - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 strategy: fail-fast: false matrix: - python-version: [3.6, 3.7, 3.8, 3.9, "3.10", "3.11"] + python-version: [3.8, 3.9, "3.10", "3.11"] steps: - uses: actions/checkout@v2 - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 with: name: ipyvue-dist-${{ github.run_number }} @@ -105,11 +105,11 @@ ui-test: needs: [build] - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v2 - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 with: name: ipyvue-dist-${{ github.run_number }} @@ -131,16 +131,16 @@ - name: Upload Test artifacts if: always() - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: ipyvue-test-results path: test-results release-dry-run: needs: [ test,ui-test,code-quality ] - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 steps: - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 with: name: ipyvue-dist-${{ github.run_number }} @@ -162,9 +162,9 @@ release: if: startsWith(github.event.ref, 'refs/tags/v') needs: [release-dry-run] - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 steps: - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 with: name: ipyvue-dist-${{ github.run_number }} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ipyvue-1.11.1/README.md new/ipyvue-1.12.0/README.md --- old/ipyvue-1.11.1/README.md 2024-05-02 10:02:04.000000000 +0200 +++ new/ipyvue-1.12.0/README.md 2026-02-11 11:02:04.000000000 +0100 @@ -26,6 +26,51 @@ $ jupyter nbextension enable --py --sys-prefix ipyvue $ jupyter labextension develop . --overwrite +Scoped CSS Support +------------------ + +`<style scoped>` in `VueTemplate` templates is supported but disabled by default for backwards +compatibility. When enabled, CSS rules only apply to the component's own elements. + +Enable globally via environment variable: + + $ IPYVUE_SCOPED_CSS_SUPPORT=1 jupyter lab + +Or in Python: + +```python +import ipyvue +ipyvue.scoped_css_support = True +``` + +Or per widget: + +```python +from ipyvue import VueTemplate + +class MyComponent(VueTemplate): + template = """ + <template> + <span class="styled">Hello</span> + </template> + <style scoped> + .styled { color: red; } + </style> + """ + +widget = MyComponent(scoped_css_support=True) +``` + +Note: The `css` trait with `scoped=True` always works, regardless of this setting: + +```python +widget = VueTemplate( + template="<template><span class='x'>Hi</span></template>", + css=".x { color: blue; }", + scoped=True +) +``` + Sponsors -------- diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ipyvue-1.11.1/examples/ScopedCSS.ipynb new/ipyvue-1.12.0/examples/ScopedCSS.ipynb --- old/ipyvue-1.11.1/examples/ScopedCSS.ipynb 1970-01-01 01:00:00.000000000 +0100 +++ new/ipyvue-1.12.0/examples/ScopedCSS.ipynb 2026-02-11 11:02:04.000000000 +0100 @@ -0,0 +1,147 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "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", + "\n", + "**How it works:** ipyvue adds a unique `data-v-*` attribute to your component's elements and rewrites your CSS selectors to include it (e.g., `.my-class` → `.my-class[data-v-abc123]`)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import ipyvue as vue\n", + "import ipywidgets as widgets\n", + "from traitlets import default" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Enable scoped CSS support\n", + "\n", + "For backwards compatibility, `<style scoped>` in templates is **disabled by default**. Existing code that accidentally relied on CSS leaking would break if we enabled it automatically.\n", + "\n", + "Enable it globally for this notebook:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Enable scoped CSS support for <style scoped> in templates\n", + "# Can also be set via environment variable: IPYVUE_SCOPED_CSS_SUPPORT=1\n", + "vue.scoped_css_support = True" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Without scoped CSS (the problem)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class GlobalStyle(vue.VueTemplate):\n", + " @default(\"template\")\n", + " def _default_template(self):\n", + " return \"\"\"\n", + " <template>\n", + " <span class=\"demo-text\">Widget A</span>\n", + " </template>\n", + " <style>\n", + " .demo-text { color: red; }\n", + " </style>\n", + " \"\"\"\n", + "\n", + "widget_b = vue.Html(tag=\"span\", children=[\"Widget B (innocent bystander)\"], class_=\"demo-text\")\n", + "\n", + "widgets.VBox([GlobalStyle(), widget_b]) # Both turn red!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## With `<style scoped>`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class ScopedStyle(vue.VueTemplate):\n", + " @default(\"template\")\n", + " def _default_template(self):\n", + " return \"\"\"\n", + " <template>\n", + " <span class=\"demo-text-2\">Widget A (scoped)</span>\n", + " </template>\n", + " <style scoped>\n", + " .demo-text-2 { color: green; }\n", + " </style>\n", + " \"\"\"\n", + "\n", + "widget_b = vue.Html(tag=\"span\", children=[\"Widget B (unaffected)\"], class_=\"demo-text-2\")\n", + "\n", + "widgets.VBox([ScopedStyle(), widget_b]) # Only Widget A is green" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Using the `css` trait with `scoped=True`\n", + "\n", + "Alternative syntax when defining CSS outside the template:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class CssTrait(vue.VueTemplate):\n", + " @default(\"template\")\n", + " def _default_template(self):\n", + " return \"<template><span class='trait-demo'>Widget C (scoped via trait)</span></template>\"\n", + "\n", + "widget_c = CssTrait(css=\".trait-demo { color: blue; }\", scoped=True)\n", + "widget_d = vue.Html(tag=\"span\", children=[\"Widget D (unaffected)\"], class_=\"trait-demo\")\n", + "\n", + "widgets.VBox([widget_c, widget_d])" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.9.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ipyvue-1.11.1/ipyvue/VueTemplateWidget.py new/ipyvue-1.12.0/ipyvue/VueTemplateWidget.py --- old/ipyvue-1.11.1/ipyvue/VueTemplateWidget.py 2024-05-02 10:02:04.000000000 +0200 +++ new/ipyvue-1.12.0/ipyvue/VueTemplateWidget.py 2026-02-11 11:02:04.000000000 +0100 @@ -1,5 +1,5 @@ import os -from traitlets import Any, Unicode, List, Dict, Union, Instance +from traitlets import Any, Bool, Unicode, List, Dict, Union, Instance, default from ipywidgets import DOMWidget from ipywidgets.widgets.widget import widget_serialization @@ -8,6 +8,7 @@ from .ForceLoad import force_load_instance import inspect from importlib import import_module +import ipyvue OBJECT_REF = "objectRef" FUNCTION_REF = "functionRef" @@ -118,6 +119,14 @@ css = Unicode(None, allow_none=True).tag(sync=True) + scoped = Bool(None, allow_none=True).tag(sync=True) + + scoped_css_support = Bool(allow_none=False).tag(sync=True) + + @default("scoped_css_support") + def _default_scoped_css_support(self): + return ipyvue.scoped_css_support + methods = Unicode(None, allow_none=True).tag(sync=True) data = Unicode(None, allow_none=True).tag(sync=True) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ipyvue-1.11.1/ipyvue/VueWidget.py new/ipyvue-1.12.0/ipyvue/VueWidget.py --- old/ipyvue-1.11.1/ipyvue/VueWidget.py 2024-05-02 10:02:04.000000000 +0200 +++ new/ipyvue-1.12.0/ipyvue/VueWidget.py 2026-02-11 11:02:04.000000000 +0100 @@ -123,7 +123,7 @@ dispatcher = self._event_handlers_map[event] # we don't call via the dispatcher, since that eats exceptions for callback in dispatcher.callbacks: - callback(self, event, data or {}) + callback(self, event, data) def _handle_event(self, _, content, buffers): event = content.get("event", "") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ipyvue-1.11.1/ipyvue/__init__.py new/ipyvue-1.12.0/ipyvue/__init__.py --- old/ipyvue-1.11.1/ipyvue/__init__.py 2024-05-02 10:02:04.000000000 +0200 +++ new/ipyvue-1.12.0/ipyvue/__init__.py 2026-02-11 11:02:04.000000000 +0100 @@ -1,3 +1,5 @@ +import os + from ._version import __version__ from .Html import Html from .Template import Template, watch @@ -10,6 +12,22 @@ ) +def _parse_bool_env(key: str, default: bool = False) -> bool: + """Parse boolean from environment variable.""" + val = os.environ.get(key, "").lower() + if val in ("1", "true", "yes", "on"): + return True + if val in ("0", "false", "no", "off"): + return False + return default + + +# Global default for scoped CSS support in VueTemplate. +# Can be set via environment variable IPYVUE_SCOPED_CSS_SUPPORT=1 +# or changed at runtime: ipyvue.scoped_css_support = True +scoped_css_support = _parse_bool_env("IPYVUE_SCOPED_CSS_SUPPORT", False) + + def _jupyter_labextension_paths(): return [ { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ipyvue-1.11.1/ipyvue/_version.py new/ipyvue-1.12.0/ipyvue/_version.py --- old/ipyvue-1.11.1/ipyvue/_version.py 2024-05-02 10:02:04.000000000 +0200 +++ new/ipyvue-1.12.0/ipyvue/_version.py 2026-02-11 11:02:04.000000000 +0100 @@ -1,2 +1,2 @@ -__version__ = "1.11.1" +__version__ = "1.12.0" semver = "^" + __version__ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ipyvue-1.11.1/js/package-lock.json new/ipyvue-1.12.0/js/package-lock.json --- old/ipyvue-1.11.1/js/package-lock.json 2024-05-02 10:02:04.000000000 +0200 +++ new/ipyvue-1.12.0/js/package-lock.json 2026-02-11 11:02:04.000000000 +0100 @@ -1,12 +1,12 @@ { "name": "jupyter-vue", - "version": "1.11.0", + "version": "1.11.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "jupyter-vue", - "version": "1.11.0", + "version": "1.11.3", "license": "MIT", "dependencies": { "@jupyter-widgets/base": "^1 || ^2 || ^3 || ^4", diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ipyvue-1.11.1/js/package.json new/ipyvue-1.12.0/js/package.json --- old/ipyvue-1.11.1/js/package.json 2024-05-02 10:02:04.000000000 +0200 +++ new/ipyvue-1.12.0/js/package.json 2026-02-11 11:02:04.000000000 +0100 @@ -1,6 +1,6 @@ { "name": "jupyter-vue", - "version": "1.11.1", + "version": "1.12.0", "description": "Jupyter widgets base for Vue libraries", "license": "MIT", "author": "Mario Buikhuizen, Maarten Breddels", diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ipyvue-1.11.1/js/src/VueTemplateModel.js new/ipyvue-1.12.0/js/src/VueTemplateModel.js --- old/ipyvue-1.11.1/js/src/VueTemplateModel.js 2024-05-02 10:02:04.000000000 +0200 +++ new/ipyvue-1.12.0/js/src/VueTemplateModel.js 2026-02-11 11:02:04.000000000 +0100 @@ -15,6 +15,7 @@ _model_module_version: '^0.0.3', template: null, css: null, + scoped: null, methods: null, data: null, events: null, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ipyvue-1.11.1/js/src/VueTemplateRenderer.js new/ipyvue-1.12.0/js/src/VueTemplateRenderer.js --- old/ipyvue-1.11.1/js/src/VueTemplateRenderer.js 2024-05-02 10:02:04.000000000 +0200 +++ new/ipyvue-1.12.0/js/src/VueTemplateRenderer.js 2026-02-11 11:02:04.000000000 +0100 @@ -9,6 +9,73 @@ import httpVueLoader from './httpVueLoader'; import { TemplateModel } from './Template'; +function normalizeScopeId(value) { + return String(value).replace(/[^a-zA-Z0-9_-]/g, '-'); +} + +function getScopeId(model, cssId) { + const base = cssId || model.cid; + return `data-s-${normalizeScopeId(base)}`; +} + +function applyScopeId(vm, scopeId) { + if (!scopeId || !vm || !vm.$el) { + return; + } + vm.$el.setAttribute(scopeId, ''); +} + +function scopeStyleElement(styleElt, scopeId) { + const scopeSelector = `[${scopeId}]`; + + function scopeRules(rules, insertRule, deleteRule) { + for (let i = 0; i < rules.length; ++i) { + const rule = rules[i]; + if (rule.type === 1 && rule.selectorText) { + const scopedSelectors = []; + rule.selectorText.split(/\s*,\s*/).forEach((sel) => { + scopedSelectors.push(`${scopeSelector} ${sel}`); + const segments = sel.match(/([^ :]+)(.+)?/); + if (segments) { + scopedSelectors.push(`${segments[1]}${scopeSelector}${segments[2] || ''}`); + } + }); + const scopedRule = scopedSelectors.join(',') + rule.cssText.substring(rule.selectorText.length); + deleteRule(i); + insertRule(scopedRule, i); + } + if (rule.cssRules && rule.cssRules.length && rule.insertRule && rule.deleteRule) { + scopeRules(rule.cssRules, rule.insertRule.bind(rule), rule.deleteRule.bind(rule)); + } + } + } + + function process() { + const sheet = styleElt.sheet; + if (!sheet) { + return; + } + scopeRules(sheet.cssRules, sheet.insertRule.bind(sheet), sheet.deleteRule.bind(sheet)); + } + + try { + process(); + } catch (ex) { + if (typeof DOMException !== 'undefined' && ex instanceof DOMException && ex.code === DOMException.INVALID_ACCESS_ERR) { + styleElt.sheet.disabled = true; + styleElt.addEventListener('load', function onStyleLoaded() { + styleElt.removeEventListener('load', onStyleLoaded); + setTimeout(() => { + process(); + styleElt.sheet.disabled = false; + }); + }); + return; + } + throw ex; + } +} + export function vueTemplateRender(createElement, model, parentView) { return createElement(createComponentObject(model, parentView)); } @@ -28,10 +95,20 @@ const isTemplateModel = model.get('template') instanceof TemplateModel; const templateModel = isTemplateModel ? model.get('template') : model; const template = templateModel.get('template'); - const vuefile = readVueFile(template); + const sourceCodeFile = `VUE_TEMPLATE_SCRIPT_${model.cid}`; + const vuefile = readVueFile(template, sourceCodeFile); const css = model.get('css') || (vuefile.STYLE && vuefile.STYLE.content); const cssId = (vuefile.STYLE && vuefile.STYLE.id); + const scopedFromTemplate = (vuefile.STYLE && vuefile.STYLE.scoped); + const scoped = model.get('scoped'); + const scopedCssSupport = model.get('scoped_css_support'); + // If scoped trait is explicitly set, use it (for css trait with scoped=True/False) + // If scoped is not set (None), only honor <style scoped> from template if scoped_css_support is enabled + const useScoped = scoped !== null && scoped !== undefined + ? scoped + : (scopedCssSupport && scopedFromTemplate); + const scopeId = useScoped && css ? getScopeId(model, cssId) : null; if (css) { if (cssId) { @@ -42,14 +119,32 @@ style.id = prefixedCssId; document.head.appendChild(style); } - if (style.innerHTML !== css) { - style.innerHTML = css; + if (scopeId) { + if (style.innerHTML !== css || style.getAttribute('data-ipyvue-scope') !== scopeId) { + style.innerHTML = css; + scopeStyleElement(style, scopeId); + style.setAttribute('data-ipyvue-scope', scopeId); + } + } else { + // Reset innerHTML if CSS changed or if transitioning from scoped to unscoped + // (need to reset to remove the scoped CSS rule transformations) + const wasScoped = style.getAttribute('data-ipyvue-scope'); + if (style.innerHTML !== css || wasScoped) { + style.innerHTML = css; + if (wasScoped) { + style.removeAttribute('data-ipyvue-scope'); + } + } } } else { const style = document.createElement('style'); style.id = model.cid; style.innerHTML = css; document.head.appendChild(style); + if (scopeId) { + scopeStyleElement(style, scopeId); + style.setAttribute('data-ipyvue-scope', scopeId); + } parentView.once('remove', () => { document.head.removeChild(style); }); @@ -106,15 +201,18 @@ ? template : vuefile.TEMPLATE, beforeMount() { + applyScopeId(this, scopeId); callVueFn('beforeMount', this); }, mounted() { + applyScopeId(this, scopeId); callVueFn('mounted', this); }, beforeUpdate() { callVueFn('beforeUpdate', this); }, updated() { + applyScopeId(this, scopeId); callVueFn('updated', this); }, beforeDestroy() { @@ -130,7 +228,7 @@ function createDataMapping(model) { return model.keys() .filter(prop => !prop.startsWith('_') - && !['events', 'template', 'components', 'layout', 'css', 'data', 'methods'].includes(prop)) + && !['events', 'template', 'components', 'layout', 'css', 'scoped', 'scoped_css_support', 'data', 'methods'].includes(prop)) .reduce((result, prop) => { result[prop] = _.cloneDeep(model.get(prop)); // eslint-disable-line no-param-reassign return result; @@ -140,7 +238,7 @@ function addModelListeners(model, vueModel) { model.keys() .filter(prop => !prop.startsWith('_') - && !['v_model', 'components', 'layout', 'css', 'data', 'methods'].includes(prop)) + && !['v_model', 'components', 'layout', 'css', 'scoped', 'scoped_css_support', 'data', 'methods'].includes(prop)) // eslint-disable-next-line no-param-reassign .forEach(prop => model.on(`change:${prop}`, () => { if (_.isEqual(model.get(prop), vueModel[prop])) { @@ -166,7 +264,7 @@ function createWatches(model, parentView, templateWatchers) { const modelWatchers = model.keys().filter(prop => !prop.startsWith('_') - && !['events', 'template', 'components', 'layout', 'css', 'data', 'methods'].includes(prop)) + && !['events', 'template', 'components', 'layout', 'css', 'scoped', 'scoped_css_support', 'data', 'methods'].includes(prop)) .reduce((result, prop) => ({ ...result, [prop]: { @@ -313,7 +411,7 @@ }), {}); } -function readVueFile(fileContent) { +function readVueFile(fileContent, sourceURL) { const component = parseComponent(fileContent, { pad: 'line' }); const result = {}; @@ -322,17 +420,45 @@ } if (component.script) { const { content } = component.script; - const str = content - .substring(content.indexOf('{'), content.length) - .replace('\n', ' '); - - // eslint-disable-next-line no-new-func - result.SCRIPT = Function(`return ${str}`)(); + try { + // Try the new approach first: define module and exports, then evaluate the whole script as if it is a commonjs module + const module = { + exports: {} + }; + /* + Add sourceURL directive - this helps browser dev tools show better error locations. + But only add it if not already present in the content (users can add it themselves if they want). + */ + const hasSourceURL = /\/\/#\s*sourceURL\s*=/i.test(content); + const contentWithScriptPath = hasSourceURL + ? content + : content + `\n//# sourceURL=${sourceURL}`; + const scriptFunction = new Function('module', 'exports', contentWithScriptPath); + scriptFunction(module, module.exports); + result.SCRIPT = module.exports; + } catch (error) { + // Fallback to the old approach for backwards compatibility + console.warn('Failed to evaluate Vue script and find module.exports, falling back to old method, please use module.exports = { ... }'); + try { + const str = content + .substring(content.indexOf('{'), content.length) + .replace('\n', ' '); + // eslint-disable-next-line no-new-func + result.SCRIPT = Function(`return ${str}`)(); + } catch (fallbackError) { + console.warn('Failed to evaluate Vue script with both new and old methods:', fallbackError); + /* This is a bit like the old behaviour, except we assume the first error is probably correcter + moreoften than not, since the old method can fail due to a { being present before module.exports */ + throw error; + } + } } if (component.styles && component.styles.length > 0) { const { content } = component.styles[0]; - const { id } = component.styles[0].attrs; - result.STYLE = { content, id }; + const { attrs = {} } = component.styles[0]; + const { id } = attrs; + const scoped = Object.prototype.hasOwnProperty.call(attrs, 'scoped'); + result.STYLE = { content, id, scoped }; } return result; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ipyvue-1.11.1/setup.py new/ipyvue-1.12.0/setup.py --- old/ipyvue-1.11.1/setup.py 2024-05-02 10:02:04.000000000 +0200 +++ new/ipyvue-1.12.0/setup.py 2026-02-11 11:02:04.000000000 +0100 @@ -210,12 +210,10 @@ "Intended Audience :: Developers", "Intended Audience :: Science/Research", "Topic :: Multimedia :: Graphics", - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.3", - "Programming Language :: Python :: 3.4", - "Programming Language :: Python :: 3.5", - "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ], ) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ipyvue-1.11.1/tests/ui/test_template.py new/ipyvue-1.12.0/tests/ui/test_template.py --- old/ipyvue-1.11.1/tests/ui/test_template.py 2024-05-02 10:02:04.000000000 +0200 +++ new/ipyvue-1.12.0/tests/ui/test_template.py 2026-02-11 11:02:04.000000000 +0100 @@ -70,3 +70,163 @@ page_session.locator("text=Click Me").click() page_session.locator("text=Clicked").wait_for() assert last_event_data == "not-an-event-object" + + +class MyTemplateScript(vue.VueTemplate): + clicks = Int(0).tag(sync=True) + + @default("template") + def _default_vue_template(self): + return """ + <template> + <div @click="click">Clicked {{clicks}}</div> + </template> + <script> + /* test { and } in a comment, which fails in ipyvue <= 1.12.2 */ + module.exports = { + methods: { + click() { + this.clicks += 1 + } + } + } + </script> + """ + + +class MyTemplateScriptOld(vue.VueTemplate): + clicks = Int(0).tag(sync=True) + + @default("template") + def _default_vue_template(self): + return """ + <template> + <div @click="click">Clicked {{clicks}}</div> + </template> + <script> + /* in ipyvue <= 1.12.2, you could put anything before + the first curly open */ + foo.bar = { + methods: { + click() { + this.clicks += 1 + } + } + } + </script> + """ + + [email protected]( + "template_class_name", ["MyTemplateScript", "MyTemplateScriptOld"] +) +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 = { + "MyTemplateScript": MyTemplateScript, + "MyTemplateScriptOld": MyTemplateScriptOld, + }[template_class_name] + + widget = template_class() + display(widget) + + ipywidgets_runner(kernel_code, {"template_class_name": template_class_name}) + widget = page_session.locator("text=Clicked 0") + widget.wait_for() + widget.click() + page_session.locator("text=Clicked 1").wait_for() + + +class ScopedStyleTemplate(vue.VueTemplate): + @default("template") + def _default_vue_template(self): + return """ + <template> + <div class="scoped-container"> + <span id="scoped-text" class="scoped-text">Scoped text</span> + </div> + </template> + <style scoped> + .scoped-text { color: rgb(255, 0, 0); } + </style> + """ + + +def test_template_scoped_style( + ipywidgets_runner, page_session: playwright.sync_api.Page +): + def kernel_code(): + from test_template import ScopedStyleTemplate + import ipyvue as vue + import ipywidgets as widgets + from IPython.display import display + + scoped = ScopedStyleTemplate(scoped_css_support=True) + unscoped = vue.Html( + tag="span", + children=["Unscoped text"], + class_="scoped-text", + attributes={"id": "unscoped-text"}, + ) + display(widgets.VBox([scoped, unscoped])) + + ipywidgets_runner(kernel_code) + page_session.locator("#scoped-text").wait_for() + page_session.locator("#unscoped-text").wait_for() + scoped_color = page_session.eval_on_selector( + "#scoped-text", "el => getComputedStyle(el).color" + ) + unscoped_color = page_session.eval_on_selector( + "#unscoped-text", "el => getComputedStyle(el).color" + ) + 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 """ + <template> + <span id="scoped-css-text" class="scoped-css-text">Scoped css text</span> + </template> + """ + + +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)" ++++++ ipyvue-1.11.1-gh.tar.gz -> ipyvue-1.12.0.tar.gz ++++++ ++++ 21900 lines of diff (skipped)
