import os
import warnings
from html.parser import HTMLParser
from ..containers import DictCallbackProperty, ListCallbackProperty
from ..selection import SelectionCallbackProperty
from ._connect import (
connect_any,
connect_bool,
connect_choice,
connect_dict,
connect_float,
connect_int,
connect_list,
connect_text,
)
from ._log import _enable_comm_logging_if_requested
__all__ = ["autoconnect_callbacks_to_vue", "HANDLERS", "TAG_TYPE_MAP"]
HANDLERS = {
"bool": connect_bool,
"int": connect_int,
"float": connect_float,
"text": connect_text,
"selection": connect_choice,
"list": connect_list,
"dict": connect_dict,
"any": connect_any,
}
TAG_TYPE_MAP = {
"v-switch": "bool",
"v-checkbox": "bool",
"v-text-field": "text",
"v-slider": "float",
"v-range-slider": "float",
"v-select": "selection",
"v-combobox": "selection",
"v-autocomplete": "selection",
}
# Attribute names that bind a Vue template expression to a traitlet.
_BINDING_ATTRS = {"v-model", ":items", ":value.sync", "v-model.number"}
class _TemplateParser(HTMLParser):
"""Parse a Vue template and collect traitlet references from binding attributes."""
def __init__(self):
super().__init__()
self.refs = {}
def handle_starttag(self, tag, attrs):
attrs_dict = dict(attrs)
# Check for binding attributes on this tag
bindings = {k: v for k, v in attrs if k in _BINDING_ATTRS and v is not None}
if not bindings:
return
# Determine connection type: echo-type attribute overrides tag inference
echo_type = attrs_dict.get("echo-type")
if echo_type is None:
inferred = TAG_TYPE_MAP.get(tag)
if inferred is not None:
# For v-text-field with type="number", use int
if inferred == "text" and attrs_dict.get("type") == "number":
echo_type = "int"
else:
echo_type = inferred
else:
for attr_value in bindings.values():
warnings.warn(
f"Vue template has binding '{attr_value}' on unknown "
f"tag <{tag}> with no echo-type attribute — skipping. "
f'Add echo-type="..." to specify the connection type.',
stacklevel=2,
)
return
if echo_type not in HANDLERS:
warnings.warn(
f"Unknown echo-type '{echo_type}' on <{tag}> — skipping. "
f"Supported types: {', '.join(sorted(HANDLERS))}",
stacklevel=2,
)
return
for attr_value in bindings.values():
prop_name = attr_value
# Normalize selection suffixes to base property name
for suffix in ("_items", "_selected"):
if prop_name.endswith(suffix):
prop_name = prop_name[: -len(suffix)]
break
self.refs.setdefault(echo_type, set()).add(prop_name)
def _parse_template(template):
"""
Parse a Vue template string and return a dict mapping handler type
codes to sets of property names referenced in the template.
The connection type is inferred from the Vue tag (e.g. ``v-switch``
maps to ``bool``). A custom ``echo-type`` attribute on the tag
overrides the inferred type.
"""
parser = _TemplateParser()
parser.feed(template)
return parser.refs
def _resolve_template(widget):
"""
Resolve the Vue template string from a widget. Checks for a
``template_file`` class attribute (tuple of module path and filename)
or a ``template`` traitlet with a ``.template`` string attribute.
Returns None if no template can be found.
"""
for klass in type(widget).__mro__:
tf = klass.__dict__.get("template_file")
if tf is not None:
if isinstance(tf, tuple) and len(tf) == 2:
module_file, vue_filename = tf
vue_path = os.path.join(os.path.dirname(module_file), vue_filename)
if os.path.isfile(vue_path):
with open(vue_path) as f:
return f.read()
break
template = getattr(widget, "template", None)
if template is not None and hasattr(template, "template"):
return template.template
return None
def _infer_type(instance, prop_name):
"""Infer the connection type from the callback property descriptor."""
prop = getattr(type(instance), prop_name)
if isinstance(prop, SelectionCallbackProperty):
return "selection"
if isinstance(prop, ListCallbackProperty):
return "list"
if isinstance(prop, DictCallbackProperty):
return "dict"
return "any"
def _discover_properties(instance):
"""Return a refs dict for all callback properties on instance."""
refs = {}
for name in dir(instance):
if not name.startswith("_") and instance.is_callback_property(name):
wtype = _infer_type(instance, name)
refs.setdefault(wtype, set()).add(name)
return refs
def _parse_extras(extras):
"""Parse an extras/only dict into refs and transforms."""
refs = {}
transforms = {}
for prop_name, spec in extras.items():
if isinstance(spec, tuple):
wtype, to_widget, from_widget = spec
transforms[prop_name] = (to_widget, from_widget)
else:
wtype = spec
if wtype not in HANDLERS:
warnings.warn(
f"Unknown type '{wtype}' for extra property "
f"'{prop_name}' — skipping. Supported types: "
f"{', '.join(sorted(HANDLERS))}",
stacklevel=2,
)
continue
refs.setdefault(wtype, set()).add(prop_name)
return refs, transforms
[docs]
def autoconnect_callbacks_to_vue(
instance, widget, template=None, extras=None, only=None, skip=None, infer_properties_from="vue", prefix=""
):
"""
Connect callback properties on ``instance`` to traitlets on
``widget`` bidirectionally.
Parameters
----------
instance : HasCallbackProperties
The state object with callback properties.
widget : HasTraits
The ipyvuetify widget.
infer_properties_from : ``'vue'`` or ``'python'``
How to discover which properties to connect:
* ``'vue'`` (default): parse the Vue template to find
``v-model`` / ``:value.sync`` bindings and infer types from
the Vue tags (e.g. ``v-switch`` → bool, ``v-slider`` →
float). Use ``extras`` for properties the parser cannot
discover.
* ``'python'``: discover all callback properties on
``instance`` and infer types from the property descriptors
(``ListCallbackProperty`` → list, ``DictCallbackProperty``
→ dict, ``SelectionCallbackProperty`` → selection,
others → any).
template : str, optional
The Vue template string. Only used when
``infer_properties_from='vue'``. If not provided, the
template is resolved from the widget's ``template_file``
class attribute or ``template`` traitlet.
extras : dict, optional
Additional properties to connect that are not discovered
automatically. Values can be:
* A type string: ``'bool'``, ``'int'``, ``'float'``,
``'text'``, ``'selection'``, ``'list'``, ``'dict'``, or
``'any'``.
* A tuple of ``(type, to_widget, from_widget)`` to supply
custom transforms.
only : set or dict, optional
When provided, connect *only* the listed properties (skip
automatic discovery). Can be a set of property names (types
auto-inferred) or a dict with the same value format as
``extras``.
skip : set, optional
Property names to skip (no warning, no connection).
prefix : str, optional
A prefix to prepend to the widget traitlet names. For example,
with ``prefix='state_'``, a callback property ``x_min`` will
sync to a widget traitlet named ``state_x_min``. When using
``infer_properties_from='vue'``, the template should reference
the prefixed names (e.g. ``state_x_min``).
Returns
-------
dict
Mapping of property names to connection handler objects.
"""
if only is not None:
if isinstance(only, set):
refs = {}
transforms = {}
for prop_name in only:
wtype = _infer_type(instance, prop_name)
refs.setdefault(wtype, set()).add(prop_name)
else:
refs, transforms = _parse_extras(only)
elif infer_properties_from == "python":
refs = _discover_properties(instance)
transforms = {}
if extras:
extra_refs, transforms = _parse_extras(extras)
extra_props = {p for names in extra_refs.values() for p in names}
for wtype in refs:
refs[wtype] -= extra_props
for wtype, prop_names in extra_refs.items():
refs.setdefault(wtype, set()).update(prop_names)
else:
if template is None:
template = _resolve_template(widget)
if template is None:
raise ValueError(
"No Vue template found. Pass template= explicitly or "
"ensure the widget has a template_file class attribute "
"or template traitlet."
)
refs = _parse_template(template)
transforms = {}
# When a prefix is set, template bindings use prefixed names
# (e.g. state_x_min) — strip the prefix to get callback property names.
if prefix:
stripped = {}
for wtype, names in refs.items():
stripped[wtype] = {name[len(prefix) :] if name.startswith(prefix) else name for name in names}
refs = stripped
if extras:
extra_refs, transforms = _parse_extras(extras)
# Extras override any template-discovered type for the same
# property (e.g. a v-select bound to a non-selection property
# that is handled via a text transform instead).
extra_props = {p for names in extra_refs.values() for p in names}
for wtype in refs:
refs[wtype] -= extra_props
for wtype, prop_names in extra_refs.items():
refs.setdefault(wtype, set()).update(prop_names)
if skip:
for wtype in refs:
refs[wtype] -= skip
_enable_comm_logging_if_requested(widget)
connections = {}
# Create connections with initial_sync=False so traits are added
# without the sync tag, avoiding per-trait comm messages.
for wtype, prop_names in refs.items():
handler_cls = HANDLERS[wtype]
for prop_name in prop_names:
if not instance.is_callback_property(prop_name):
widget_prop_check = prefix + prop_name if prefix else prop_name
if widget.has_trait(widget_prop_check):
continue
warnings.warn(
f"Vue template references '{prop_name}' (type={wtype}) "
f"but '{prop_name}' is not a callback property on "
f"{type(instance).__name__}",
stacklevel=2,
)
continue
to_w, from_w = transforms.get(prop_name, (None, None))
if prefix:
if wtype == "selection":
widget_prop = prefix + prop_name + "_selected"
else:
widget_prop = prefix + prop_name
else:
widget_prop = None
handler = handler_cls(
instance,
prop_name,
widget,
widget_prop=widget_prop,
to_widget=to_w,
from_widget=from_w,
initial_sync=False,
)
connections[prop_name] = handler
# Set the initial values, enable sync, and send all state in one
# comm message rather than one per trait.
sync_keys = set()
for handler in connections.values():
handler._from_state()
handler.enable_widget_sync()
sync_keys.update(handler._sync_trait_names())
if hasattr(widget, "send_state") and sync_keys:
widget.send_state(key=sync_keys)
# Register hold_sync so that delay_callback batches widget updates
# into a single comm message instead of one per property.
if hasattr(widget, "hold_sync") and hasattr(instance, "_notify_context_managers"):
instance._notify_context_managers.append(widget.hold_sync)
return connections