diff --git a/.gitignore b/.gitignore index dfd9a91..becd08a 100644 --- a/.gitignore +++ b/.gitignore @@ -83,3 +83,8 @@ examples site _site .mypy_cache + +# optional Cython build (setup_cython.py) +/build/ +/reacton/_fastcore.c +/reacton/_fastcore.html diff --git a/benchmarks/README.md b/benchmarks/README.md index 16b773c..5dff36c 100644 --- a/benchmarks/README.md +++ b/benchmarks/README.md @@ -65,9 +65,26 @@ renderer (`_RenderContextFast`, `REACTON_FAST=1`) addresses both: identical child widget objects (`_values_identical`), avoiding pointless traitlets assignments. Container widgets keep the kwargs they were last created or updated with (`resolved_kwargs`) for this compare. -- **Mount path.** The implicit container is only built when a component body - returns `None`; component context managers are entered without an - `ExitStack`; the child visitors dispatch on exact types and skip scalars. +- **Mount path.** A new component (first render, new list item, another + component type at a key) is mounted in one walk (`_fastcore.mount_component`): + bodies run as in the render phase, widgets are made children first. A mounted + component (`_MountedContext`) keeps its element tree positionally (`nodes`: + the widgets and child contexts in the order they were made); the dicts of the + update paths (`elements`, `widgets`, `children`, `element_to_widget`, + `used_keys`, `resolved_kwargs`, ...) are made from it, with the same keys, when + they are first used (`_fastcore.materialize`), and a mounted subtree that goes + away is removed from it (`_fastcore.remove_mounted`). A pass that cannot keep + its mounts (state set or an exception during the mount, shared elements, a + widget that fails to be made) undoes them into the two phase bookkeeping. The + implicit container is only built when a component body returns `None`. +- **Compiled core (optional).** `reacton/_fastcore.py` holds the element base + classes, `render_fixed` and the first render of a new render context + (`render_first`: it mounts the root without the walks of `render()`, whose loop + takes over when more passes are needed), the mount, the hooks and the + listener/setter objects. It is plain + Python; `python setup_cython.py build_ext --inplace` compiles it with Cython + (pure Python mode, types in `_fastcore.pxd`). `REACTON_CYTHON=0` forces the + plain version when a compiled one is present. - **Side-effect ("orphan") widgets** (Layout/Style created during construction) are tracked via ipywidgets' `on_widget_constructed` hook instead of diffing the global widgets dict per creation — the old diff was O(live widgets) per @@ -107,7 +124,8 @@ random state changes and compares the widgets and the effect order. **Keys.** `el._key` or a positional default. A context's root key is `"/"`. Children of an element with key `K`: list → `f"{K}{i}/"`, dict → `f"{K}{k}/"`. Component child contexts live in `context.children[key]`. A duplicate key in one -context raises `KeyError`. `el._key_frozen` is set once an element is rendered. +context raises `KeyError`. `el._key_frozen` is true once an element is rendered +(`el._render_count > 0`). **Render phase, per element:** - `el._render_count += 1` (a shared element is visited once; a non-shared one diff --git a/reacton/_fastcore.pxd b/reacton/_fastcore.pxd new file mode 100644 index 0000000..5bb1511 --- /dev/null +++ b/reacton/_fastcore.pxd @@ -0,0 +1,230 @@ +# Cython declarations for _fastcore.py (pure Python mode). Everything that makes the compiled +# module fast is declared here, so the .py runs as plain Python without paying for it. +# +# Do NOT declare these module globals here (keep them plain module globals): reacton.core +# assigns them from Python when solara writes reacton.core._default_container and +# reacton.core._component_context_manager_classes (see reacton.core._CoreModule), and +# reacton.core.DEBUG; and reacton.core writes _provides and _log_debug. +cimport cython +from libc.stdlib cimport getenv +from cpython.object cimport PyTypeObject +from cpython.ref cimport PyObject + + +cdef tuple _EMPTY_TUPLE = () + + +cdef inline object _object_new(object cls): + # object.__new__(cls), directly (cls must not override __new__) + return (cls).tp_new(cls, _EMPTY_TUPLE, NULL) + + +cdef inline bint _getenv_fast(): + # REACTON_FAST=1 + cdef const char* value = getenv(b"REACTON_FAST") + return value != NULL and value[0] == 49 and value[1] == 0 + + +cdef class ContainerAdder: + cdef dict __dict__ + cdef object __weakref__ + cdef public object el, prop_name + cdef public list created + + +# (no __dict__ and __weakref__ here: the Python subclasses in reacton.core get them, and make +# the dict when an attribute is first set; a dict declared here is made for every element) +cdef class ElementBase: + cdef public object component + cdef public object args + cdef public object kwargs + cdef public object mime_bundle + cdef public object _key + cdef public object _meta + cdef public object _on_kwargs + cdef public object _leaf + cdef public object _event_handlers + cdef public bint is_shared + cdef public Py_ssize_t _render_count + + +cdef class ValueElementBase(ElementBase): + cdef public object value_property + + +# ---- the mount (reacton.core._RenderContextFast): contexts and the render context are plain +# Python objects (the update paths use them from Python), elements are typed + +cdef class _Mount: + cdef public object rc + cdef public list recording + cdef public list order + cdef public bint failed + cdef public Py_ssize_t raised + cdef public set shared_next + cdef public dict keys + cdef public list adders + cdef public ContainerAdder body_adder + cdef public object top + + +cdef object _new_instance(object cls) + +# (only _fastcore uses these) +cdef dict _plain_classes +cdef dict _logger_cache +cdef frozenset _SCALAR_TYPES + +cdef int _plain_class(object cls) except -1 +cdef class _WidgetInfo: + cdef public object widget + cdef public frozenset trait_names + cdef public object batched + +@cython.locals(info=_WidgetInfo) +cdef _WidgetInfo _widget_info(object component) + +@cython.locals(m=_Mount, c=object, widget=object) +cpdef object mount_component(object rc, ElementBase el, object key, object parent_context, object order, object context) + +cdef object _new_context(object parent) +cdef object _adopt(object precreated, object parent) + +@cython.locals(rc=object, precreated_children=object, nodes=list, managers=object, raised=Py_ssize_t, provides=Py_ssize_t, root=object, widget=object, adders=list, user_contexts=object) +cdef object _mount_component(_Mount m, ElementBase el, object parent, list parent_nodes, object context, object key) + +@cython.locals(root=object) +cdef object _call_body(_Mount m, ElementBase el, object managers) + +@cython.locals(component=object, default_container=object, created=list, root_element=object, kwargs=dict, container=object) +cpdef object call_component(list container_adders, ContainerAdder adder, ElementBase el) + +@cython.locals(key=object, all_keys=dict, keys=set, component=object, precreated=object, precreated_children=object, child=object, widget=object, kwargs=dict, resolved=dict, name=object, value=object, new_value=object, rc=object, element_class=object, plain=int, added=object, listener=object, info=_WidgetInfo, recording=list, count=Py_ssize_t, listeners=dict, traits=frozenset, callback=object, widget_class=object, handlers=tuple, handler=object, orphan_ids=object, widgets_dict=object, maybe_listener=bint, t=object) +cdef object _mount_node(_Mount m, ElementBase el, object c, list nodes, object dkey) + +@cython.locals(values=list, index=Py_ssize_t, x=object, w=object) +cdef list _mount_list(_Mount m, object value, object c, list nodes, object dkey) + +cdef object _mount_value(_Mount m, object value, object c, list nodes, object dkey) + +cpdef object init_context(object c) + +@cython.locals(root=object) +cpdef object init_render_context(object rc, object element, object container, object children_trait, object handle_error, bint fast) + +@cython.locals(reasons=object) +cpdef add_rerender_reason(object rc, object reason) + +cpdef bint fast_selected() + +@cython.locals(enabled=object) +cdef object _info_enabled() + +@cython.locals(rc=object, widget=object) +cpdef object render_fixed(object element, object handle_error=*) + +@cython.locals(root=object, lock=object, widget=object, prev_rc=object, key=object, more=bint) +cpdef object render_first(object rc, object element, object container) + +@cython.locals(order=list, raised=bint, context=object, effects=object, parent=object, effect=object, widget=object, el=ElementBase, key=object) +cpdef object finish_mount(object rc, object root) + +@cython.locals(node=object, parent=object) +cdef object _bubble_exceptions(object c) + +@cython.locals(nodes=list, w=_Materialize, root=object) +cpdef object materialize(object c) + +cdef class _Materialize: + cdef public bint partial + cdef public list nodes + cdef public Py_ssize_t index + cdef public Py_ssize_t order + cdef public dict elements + cdef public dict widgets + cdef public dict children + cdef public dict element_to_widget + cdef public set used_keys + cdef public dict resolved_kwargs + + @cython.locals(key=object, component=object, child=object, widget=object, resolved=dict, start=Py_ssize_t, name=object, value=object, new_value=object, node=object, traits=frozenset, callback_wrappers=dict, listener=object, added=object) + cpdef object node(self, ElementBase el, object default_key) + + @cython.locals(t=object, values=list, index=Py_ssize_t, x=object, w=object) + cpdef object value(self, object value, object key) + + +@cython.locals(el=object, widget=object, added=object, mounted_listeners=bint, listener=object, orphans=object, orphan=object, orphan_widget=object, close=object, widgets_dict=object) +cdef object _close_widget_node(object rc, object node) + +@cython.locals(errors=list) +cpdef object remove_mounted(object rc, object child_context, bint closing) + +@cython.locals(context=object, effect=object, cleanup=object, handler=object, nodes=list, node=object, switched=bint, effects=object, handlers=object, errors=list, errors_children=list, sub=list) +cdef list _remove_mounted(object rc, object child_context, bint closing) + + +# ---- the hooks + +cdef class RefBase: + cdef public object current + + +cdef class _EventHandler: + cdef public object rc, context, callback, event, widget, registered_event + cdef public bint removed + + +@cython.locals(rc=object) +cpdef use_state(initial, key=*, eq=*) + +@cython.locals(context=object, index=Py_ssize_t, state=dict, value=object, setters=dict, setter=object, eq_cell=list) +cpdef rc_use_state(rc, initial, key, eq) + +cpdef rc_use_ref(rc, initial_value) + +@cython.locals(rc=object) +cpdef use_ref(initial_value) + +@cython.locals(memo=object, index=Py_ssize_t, value=object, dependencies_previous=object) +cdef object _use_ref(object context, object initial_value) + +@cython.locals(rc=object) +cpdef use_memo(f, dependencies=*, debug_name=*) + +@cython.locals(context=object, name=object, memo=object, index=Py_ssize_t, value=object, entry=tuple, dependencies_previous=object) +cpdef rc_use_memo(rc, f, dependencies, debug_name) + +@cython.locals(rc=object) +cpdef use_effect(effect, dependencies=*) + +@cython.locals(new=object) +cdef object _new_effect(object callable, object dependencies) + +@cython.locals(rc=object, context=object, value=object, user_contexts=object) +cpdef use_context(user_context) + +cdef class _ContextListener: + cdef public object set_counter + +cdef class _ContextConnect: + cdef public object context, user_context, listener + +@cython.locals(context=object, effects=object, index=Py_ssize_t, previous_effect=object) +cpdef rc_use_effect(rc, effect, dependencies) + +@cython.locals(vue=object) +cdef object _is_vue(object component) + +@cython.locals(rc=object, context=object, ref=object, handler=object, component=object, events=object, handlers=tuple) +cpdef use_event(el, event_and_modifiers, callback) + + +cdef class _Listener: + cdef public object rc, context, name, widget, callback + + +cdef class _Setter: + cdef public object rc, context, key + cdef public object eq + cdef public object created_stack diff --git a/reacton/_fastcore.py b/reacton/_fastcore.py new file mode 100644 index 0000000..2cfd4ea --- /dev/null +++ b/reacton/_fastcore.py @@ -0,0 +1,2039 @@ +"""The hot building blocks of reacton, written for Cython's pure Python mode. + +This module is plain Python and works as it is (PyPy, no compiler, development). When it is +compiled (``python setup_cython.py build_ext --inplace``, see ``_fastcore.pxd``), the same +source becomes a C extension: elements become extension types with typed fields, and the +code that makes and walks them runs without the interpreter. Rules for this file: + +- all typing lives in ``_fastcore.pxd``: the .py must not pay for it when it is not compiled + (``cython.cast``/``cython.declare``/decorators are real calls in plain Python); +- class defaults that are typed fields when compiled go in an ``if not cython.compiled:`` + block of the class body (it runs once, at import), the compiled defaults in ``__cinit__`` + (never called when not compiled); +- no closures inside functions that the .pxd declares ``cpdef``. + +reacton.core builds the public classes on top of these (``Element`` is a Python subclass of +``ElementBase``), so everything that user code sees stays a normal Python class. +""" + +import os +import sys +import threading +import weakref +from collections import deque +from types import TracebackType +from typing import Any, Dict, List, Optional # noqa: F401 (List: in a type comment) + +try: + import cython +except ImportError: # plain Python without Cython installed + # (through globals(): a plain assignment would redeclare the name for the compiler) + globals()["cython"] = type("cython", (), {"compiled": False}) + + +from . import utils + +# the render context of the thread that renders (local.rc), see reacton.core +local = threading.local() +# set together with reacton.core.DEBUG: keep the stack where every element was made +DEBUG = 0 + +widget_render_error_msg = ( + """Cannot show widget. You probably want to rerun the code cell above (Click in the code cell, and press Shift+Enter ⇧+↩).""" +) +mime_bundle_default: Dict[str, Any] = {"text/plain": "Cannot show ipywidgets in text", "text/html": widget_render_error_msg} +# the default meta of an element (never changed in place: meta() makes a new dict) +_NO_META: dict = {} +# reacton.core's Element and ValueElement (see _register) +_Element: Any = None +_ValueElement: Any = None +# reacton.core._default_container (solara sets it; reacton.core forwards the assignment) +_default_container: Any = None +# reacton.core._component_context_manager_classes (the same list) +_component_context_manager_classes: list = [] +# the list of a ContainerAdder that is not on the stack (never appended to) +_NO_ELEMENTS: list = [] +# counts provide() calls (UserContext.provide): a mounted component only looks at its +# user_contexts when its body provided something +_provides = 0 + + +def _register(element_class, value_element_class): + global _Element, _ValueElement + _Element = element_class + _ValueElement = value_element_class + + +def find_elements(value): + if isinstance(value, ElementBase): + el = value + elements = {el} + if not isinstance(el.kwargs, dict): + raise RuntimeError("keyword arguments for {el} should be a dict, not {el.kwargs}") + elements |= find_elements(el.args) + elements |= find_elements(el.kwargs) + return elements + elif isinstance(value, (tuple, list)): + elements = set() + for child in value: + if isinstance(child, (ElementBase, tuple, list, dict)): + elements |= find_elements(child) + return elements + elif isinstance(value, dict): + elements = set() + for child in value.values(): + if isinstance(child, (ElementBase, tuple, list, dict)): + elements |= find_elements(child) + return elements + + +class ContainerAdder: + """Collects the elements made inside a ``with element:`` block (and a component body). + + Every new element is added to the innermost one on ``rc.container_adders``: to ``created`` + for this class, through ``add()`` for any other kind of adder. + """ + + def __init__(self, el, prop_name): + self.el = el + self.prop_name = prop_name + self.created = [] + + def __class_getitem__(cls, item): + # (ContainerAdder[W](...), it used to be a typing.Generic) + return cls + + def add(self, el): + self.created.append(el) + + def collect(self): + children = set() + for el in self.created: + children |= find_elements(el) - {el} + top_level = [k for k in self.created if k not in children] + return top_level + + +class ElementBase: + """The data of an element (reacton.core.Element adds the rest).""" + + if not cython.compiled: + # (compiled: typed fields, see _fastcore.pxd, with the defaults set in __cinit__) + component: Any + args: Any + kwargs: Dict[str, Any] + # a plain attribute (not a property): it is read for every element in every walk + is_shared: bool = False + # Defaults as class attributes: every component body makes elements, most of them + # never change these. (_meta is never changed in place, meta() makes a new dict.) + mime_bundle: Dict[str, Any] = mime_bundle_default + _key: Optional[str] = None + _meta: Dict[str, Any] = _NO_META + # how often the element was rendered (also for testing), see _key_frozen + _render_count: int = 0 + # facts about the kwargs of a widget element, learned when its widget is created or + # updated (None: not known), so a close of the whole tree can skip work + _on_kwargs: Optional[bool] = None # a kwarg starts with on_ (maybe an event listener) + _leaf: Optional[bool] = None # no elements in the kwargs + # handlers (of reacton.ipyvue.use_event) to register on the widget of this element + # when it is created or updated: objects with _reacton_attach(widget) + _event_handlers: tuple = () + # (only in DEBUG mode: where the element was made) + traceback: TracebackType + + def __cinit__(self): + # (compiled only: the defaults of the typed fields) + self.mime_bundle = mime_bundle_default + self._meta = _NO_META + self._event_handlers = () + + def __init__(self, component, args=None, kwargs=None): + self.component = component + self.args = args or [] + self.kwargs = kwargs or {} + # the elements made in a `with container:` block (or a component body) go there + rc = getattr(local, "rc", None) + if rc is not None: + container_adders = rc.container_adders + if container_adders: + adder = container_adders[-1] + if type(adder) is ContainerAdder: + ca: ContainerAdder = adder + ca.created.append(self) + else: + adder.add(self) + if DEBUG: + _keep_traceback(self) + + @property + def _key_frozen(self): + # rendered at least once. The renderers used to set a flag next to every + # _render_count += 1, one attribute write per element render. + return self._render_count > 0 + + def key(self, value): + """Returns the same element with a custom key set. + + This can help render performance. See documentation for details. + """ + if self._render_count: + raise RuntimeError("Element keys should not be mutated after rendering") + self._key = value + return self + + def meta(self, **kwargs): + """Add metadata to the created widget. + + This can be used to find a widget for testing. + """ + self._meta = {**self._meta, **kwargs} + return self + + # the old name of is_shared + @property + def _shared(self): + return self.is_shared + + @_shared.setter + def _shared(self, value): + self.is_shared = value + + def shared(self): + self.is_shared = True + return self + + def _arguments_changed(self, other): + # called for every child of a component that renders again: the same objects + # (small ints, interned strings, the same callbacks) need no utils.equals call + args = self.args + other_args = other.args + kwargs = self.kwargs + other_kwargs = other.kwargs + if args: + if len(args) != len(other_args): + return True + elif other_args: + return True + if kwargs: + if len(kwargs) != len(other_kwargs): + return True + for k, v in kwargs.items(): + if k not in other_kwargs: + return True + other_v = other_kwargs[k] + if v is not other_v and not utils.equals(v, other_v): + return True + elif other_kwargs: + return True + if args: + for a, b in zip(args, other_args): + if a is not b and not utils.equals(a, b): + return True + return False + + def __reduce__(self): + # (explicit, the same in both modes: a compiled element has no automatic pickling) + return (_rebuild_element, (type(self), _element_state(self))) + + +class ValueElementBase(ElementBase): + """An element for a widget with a value (reacton.core.ValueElement adds the rest).""" + + if not cython.compiled: + value_property: str + + def __init__(self, value_property, component, args=None, kwargs=None): + self.value_property = value_property + # ElementBase.__init__, inline + self.component = component + self.args = args or [] + self.kwargs = kwargs or {} + rc = getattr(local, "rc", None) + if rc is not None: + container_adders = rc.container_adders + if container_adders: + adder = container_adders[-1] + if type(adder) is ContainerAdder: + ca: ContainerAdder = adder + ca.created.append(self) + else: + adder.add(self) + if DEBUG: + _keep_traceback(self) + + +_ELEMENT_FIELDS = ( + "component", + "args", + "kwargs", + "is_shared", + "mime_bundle", + "_key", + "_meta", + "_render_count", + "_on_kwargs", + "_leaf", + "_event_handlers", + "value_property", +) +_MISSING = object() + + +def _element_state(el): + state = {} + for name in _ELEMENT_FIELDS: + value = getattr(el, name, _MISSING) + if value is not _MISSING: + state[name] = value + state.update(el.__dict__) + return state + + +def _rebuild_element(cls, state): + el = cls.__new__(cls) + for name, value in state.items(): + setattr(el, name, value) + return el + + +def _keep_traceback(el, depth=-1): + # DEBUG: keep the frame of the code that made the element (the caller of the factory), to + # show where an element came from when rendering it fails. Compiled functions have no + # frame of their own, so the frame of that code is closer to the top of the stack. + # (depth -1: called from ElementBase.__init__, which is called by a factory) + if depth == -1: + depth = 1 if cython.compiled else 3 + frame = sys._getframe(depth) + el.traceback = TracebackType(tb_next=None, tb_frame=frame, tb_lasti=frame.f_lasti, tb_lineno=frame.f_lineno) + + +def component_call(self, *args, **kwargs): + # ComponentFunction.__call__: make the element of a component + if self.value_name is not None: + el = _ValueElement(self.value_name, self, args, kwargs) + else: + # ElementBase.__init__, inline (no type call). (A local annotation: not evaluated + # without Cython; the .pxd cannot declare a function with *args.) + new: ElementBase = ElementBase.__new__(_Element) + new.component = self + new.args = args or [] + new.kwargs = kwargs + rc = getattr(local, "rc", None) + if rc is not None: + container_adders = rc.container_adders + if container_adders: + adder = container_adders[-1] + if type(adder) is ContainerAdder: + ca: ContainerAdder = adder + ca.created.append(new) + else: + adder.add(new) + el = new + if self.mime_bundle is not mime_bundle_default: + el.mime_bundle = self.mime_bundle + if DEBUG: + # the code that called the component (see _keep_traceback) + _keep_traceback(el, 0 if cython.compiled else 2) + return el + + +def component_widget_new(cls, widget, mime_bundle=mime_bundle_default): + """ComponentWidget.__new__ (see there).""" + if cls is _ComponentWidget and mime_bundle is mime_bundle_default: + try: + self = widget._reacton_component_widget + except AttributeError: + pass + else: + # (a subclass inherits the attribute of its base class) + if self.widget is widget: + return self + self = object.__new__(cls) + if mime_bundle is not mime_bundle_default: + self.mime_bundle = mime_bundle + self.widget = widget + self.name = widget.__name__ + if cls is _ComponentWidget and mime_bundle is mime_bundle_default: + try: + widget._reacton_component_widget = self + except (AttributeError, TypeError): + # a class that does not take attributes: no cache + pass + return self + + +# ============================================================================================ +# The mount of the fast renderer (reacton.core._RenderContextFast) +# +# A new component (a first render, a new list item, another component type at a key) is +# mounted in one walk: the bodies run as in the render phase, and the widgets of the new +# subtree are made children first. A mounted component (a _MountedContext) keeps its element +# tree positionally: `nodes` holds, in the order the widgets were made (children first), the +# widget of each widget element and the context of each component element. The dicts the +# update paths use (elements, widgets, children, element_to_widget, used_keys, +# resolved_kwargs, ...) are made from that only when they are used (materialize), with the +# same keys as the two phase walk. Reconciliation of the pass runs the effects (finish_mount). +# +# When the pass cannot keep the mounted widgets (a body sets state or raises, a shared +# element, a widget that fails to be made, an explicit key that could match a positional key), +# the mounts of the pass are undone (undo_mounts): the widgets are closed and the contexts +# get the render bookkeeping of the two phase walk, which then takes over. +# ============================================================================================ + +# what the mount uses from reacton.core (set by _register_core) +_core: Any = None +_ComponentFunction: Any = None +_ComponentWidget: Any = None +_ComponentContext: Any = None +_MountedContext: Any = None +_FragmentWidget: Any = None +_logger: Any = None +_logger_cache: Any = None +_RenderContextFast: Any = None +# values that cannot hold elements (the child visitors skip them) +_SCALAR_TYPES = frozenset([str, int, float, bool, complex, bytes, type(None)]) +# the dicts of a component context that are made from its nodes (see materialize) +_MATERIALIZED = frozenset(["elements", "widgets", "children", "element_to_widget", "used_keys", "resolved_kwargs", "elements_next", "children_next"]) +# the slots of reacton.core._MountedContext +_MOUNTED_SLOTS = ( + "nodes", + "compact_widget", + "elements", + "widgets", + "children", + "element_to_widget", + "used_keys", + "resolved_kwargs", + "elements_next", + "children_next", +) +# element classes that use the Element methods to make and remove a widget (see _plain_class) +_plain_classes: dict = {} +_EMPTY: dict = {} + + +def _new_instance(cls): + # cls.__new__(cls) for a Python class that does not override __new__ (compiled: without + # the lookup and the checks of object.__new__, see _fastcore.pxd) + if not cython.compiled: + return cls.__new__(cls) + return _object_new(cls) # type: ignore[name-defined] # noqa: F821 (see _fastcore.pxd) + + +def _register_core(core): + global _core, _ComponentFunction, _ComponentWidget, _ComponentContext, _MountedContext, _FragmentWidget, _logger, _RenderContextFast, _logger_cache + _core = core + _ComponentFunction = core.ComponentFunction + _ComponentWidget = core.ComponentWidget + _ComponentContext = core.ComponentContext + _MountedContext = core._MountedContext + _FragmentWidget = core.FragmentWidget + _logger = core.logger + _logger_cache = getattr(_logger, "_cache", None) + _RenderContextFast = core._RenderContextFast + _register_hooks(core) + + +def _plain_class(cls): + # 0: an element class that overrides how its widget is made or removed (its own methods are + # called); 1: it makes and removes its widget with the Element methods (the mount does that + # inline); 2: also its event listeners + plain = _plain_classes.get(cls, -1) + if plain == -1: + element = _core.Element + plain = 0 + if ( + cls._create_widget is element._create_widget + and cls._close_widget is element._close_widget + and cls._cleanup_callbacks is element._cleanup_callbacks + and cls._split_kwargs is element._split_kwargs + and cls._get_widget_args is element._get_widget_args + ): + plain = 2 if cls._add_widget_event_listener is element._add_widget_event_listener else 1 + _plain_classes[cls] = plain + return plain + + +class _WidgetInfo: + """What the mount keeps about the widget class of a ComponentWidget (on the component).""" + + def __init__(self, component): + self.widget = component.widget + self.trait_names = frozenset(component.widget.class_trait_names()) + # the widget class whose hold_trait_notifications batches renders (see + # core._install_batched_hold) + self.batched = None + + +def _widget_info(component): + info = component._reacton_info + if info is None: + info = component._reacton_info = _WidgetInfo(component) + return info + + +class _Mount: + """The walk that mounts one new subtree (see mount_component).""" + + def __init__(self, rc): + self.rc = rc + # the widgets made during the mount (to find those made as a side effect, like Layout) + self.recording = [] + # the contexts of this mount, children first (effects run in this order) + self.order = [] + self.failed = False + # bodies that raised in this mount (then exceptions bubble up, see _mount_component) + self.raised = 0 + self.shared_next = rc._shared_elements_next + # context -> the explicit keys in its tree (the duplicate check) + self.keys = None + self.adders = rc.container_adders + self.body_adder = rc._body_adder + # the context of the top of the mount + self.top = None # type: Any + + +def mount_component(rc, el, key, parent_context, order, context): + """Mount the new component element el at key in parent_context (which renders in two phases). + + context: a context made by state_set (restored state), or None. Returns the root widget, + or None when this pass cannot keep the mount. + """ + m = _Mount(rc) + if rc._rerender_needed: + # (state set earlier in this pass: it will be undone, do not make widgets) + m.failed = True + previous_recording = _core._start_recording_constructed(m.recording) + try: + widget = _mount_component(m, el, parent_context, None, context, key) + c = m.top + c.order_in_parent = order + finally: + # (reacton.core._stop_recording_constructed, inline) + _core._construction_local.recording = previous_recording + if widget is not None and not m.failed: + c.mount_order = m.order + rc._mount_roots[c] = None + else: + # parts of the subtree were mounted, not all: undo at the end of the pass + rc._mount_failed = True + return widget + + +def _new_context(parent): + # (compact_widget is set when the root widget is made) + c = _new_instance(_MountedContext) + c.parent = parent + c.nodes = [] + return c + + +def _adopt(precreated, parent): + # a context made by state_set (restored state) is mounted as a new one with its state; the + # component elements in its tree look up the pre-made contexts of their keys + context = _new_context(parent) + context.state = precreated.state + context.setters = precreated.setters + precreated_children = precreated.__dict__.get("children_next") + context.precreated_children = precreated_children if precreated_children else None + return context + + +def _mount_component(m, el, parent, parent_nodes, context, key): + # Returns the root widget (None when this pass cannot keep the mount). parent_nodes: the + # nodes of the parent (None for the top of a mount: key is its key in a parent that renders + # in two phases) + rc = m.rc + precreated_children = None + if context is None: + context = _new_instance(_MountedContext) + context.parent = parent + nodes = context.nodes = [] + else: + context = _adopt(context, parent) + precreated_children = context.precreated_children + nodes = context.nodes + context.invoke_element = el + if key is not None: + # the top of the mount + context.key_in_parent = key + parent.children_next[key] = context + rc._mount_tops.append(context) + m.top = context + else: + parent_nodes.append(context) + managers = None + if _component_context_manager_classes: + managers = context.context_managers = [cm(el) for cm in _component_context_manager_classes] + adders = m.adders + if adders: + del adders[:] + rc.context = context + raised = m.raised + provides = _provides + root = None + try: + root = _call_body(m, el, managers) + except BaseException as e: + _logger.exception("Component %r raised exception %r", el.component, e) + context.exceptions_self.append(e) + rc._set_rerender_needed("Exception ocurred during render") + context.needs_render = True + m.raised += 1 + # (a nested render() from a body is refused by render() itself: it holds the lock) + if rc._rerender_needed: + # the body set state (or raised): this pass will be undone, do not make more widgets + # (only a body changes the flag, so the widgets do not check it themselves) + m.failed = True + widget = None + if root is not None: + if not isinstance(root, ElementBase): + raise TypeError(f"Expected element, not {root}") + if el._event_handlers: + _core._add_event_handlers(root, el._event_handlers, context, rc) + context.root_element = root + widget = _mount_node(m, root, context, nodes, "/" if precreated_children else None) + elif el.is_shared: + m.shared_next.discard(el) + rc.context = parent + if precreated_children is not None: + # pre-made (state_set) children that were not used + context.precreated_children = None + if _provides != provides: + user_contexts = context.user_contexts + if user_contexts is not _EMPTY: + # (provide() made them) + context.user_contexts_prev = user_contexts + if m.raised != raised: + # exceptions in this subtree: as in _render_component + if context.exceptions_self or context.exceptions_children and not context.exception_handler: + parent.exceptions_children.extend(context.exceptions_self) + parent.exceptions_children.extend(context.exceptions_children) + if context.exceptions_self or context.exceptions_children: + rc._mark_dirty(context) + if parent.exceptions_self or parent.exceptions_children: + if not rc._rerender_needed: + rc._set_rerender_needed("Exception ocurred during render") + parent.needs_render = True + if widget is not None: + context.compact_widget = widget + if context.effects: + # (read now: the context is in the cache; finish_mount only visits these) + m.order.append(context) + return widget + + +def _call_body(m, el, managers): + # the component function, inside its context managers (solara registers one) + if not managers: + root = call_component(m.adders, m.body_adder, el) + assert root is not None + elif len(managers) == 1: + with managers[0]: + root = call_component(m.adders, m.body_adder, el) + assert root is not None + else: + import contextlib + + with contextlib.ExitStack() as stack: + for manager in managers: + stack.enter_context(manager) + root = call_component(m.adders, m.body_adder, el) + assert root is not None + return root + + +def call_component(container_adders, adder, el): + """Run the component function, with an implicit container when it returns None. + + container_adders: rc.container_adders, adder: the ContainerAdder of the bodies of the render + context (reacton.core._RenderContextFast._call_component). + """ + component = el.component + default_container = _default_container + if default_container is None: + component.render_count += 1 + return component.f(*el.args, **el.kwargs) + # Only a body that returns None needs the implicit container. Building it for every body + # (an extra element, and collecting the top level elements from all elements the body + # made) costs more than a typical component body, so first only record the elements the + # body makes, like the container would. + # (the adder's list is reused: bodies of one render context do not nest) + created = adder.created + if created: + del created[:] + container_adders.append(adder) + try: + component.render_count += 1 + kwargs = el.kwargs + if kwargs: + root_element = component.f(*el.args, **kwargs) + else: + root_element = component.f(*el.args) + finally: + container_adders.pop() + if root_element is None: + with default_container() as container: + # the container collects the same elements, the same way + container_adders[-1].created.extend(created) + if len(container.kwargs["children"]) == 1: + root_element = container.kwargs["children"][0] + else: + root_element = container + if created: + del created[:] + return root_element + + +def _mount_node(m, el, c, nodes, dkey): + # The mount walk of an element in the tree of the component context c (nodes: its nodes); + # returns its widget (None when this pass does not make widgets any more). dkey: the + # positional key of el, only in a context with pre-made children (else None: keys are + # made when needed). + key = el._key + if key is not None: + all_keys = m.keys + if all_keys is None: + all_keys = m.keys = {} + keys = all_keys.get(c) + if keys is None: + all_keys[c] = {key} + elif key in keys: + raise KeyError(f"Duplicate key {key!r}") + else: + keys.add(key) + if "/" in key: + # could be the same as a positional key: the two phase walk checks all keys + m.failed = True + else: + key = dkey + if el.is_shared: + # rendered once for the whole tree, by the two phase walk + m.failed = True + c.has_shared = True + if el in m.shared_next: + return None + m.shared_next.add(el) + el._render_count += 1 # (also freezes the key, see Element._key_frozen) + component = el.component + if type(component) is _ComponentFunction or (type(component) is not _ComponentWidget and not isinstance(component, _ComponentWidget)): + # a component element + if el.is_shared and (el.args or el.kwargs): + # the arguments of a shared element belong to the context it is rendered in + _mount_value(m, el.kwargs, c, nodes, key) + _mount_value(m, el.args, c, nodes, key) + precreated = None + if dkey is not None: + # (a context with pre-made children, see _adopt) + precreated_children = c.precreated_children + if precreated_children: + precreated = precreated_children.pop(key, None) + widget = _mount_component(m, el, c, nodes, precreated, None) + if widget is not None and el._meta: + widget._react_meta = {**getattr(widget, "_react_meta", {}), **el._meta} + return widget + + # a widget element: first the elements in its kwargs (children first) + assert not el.args, "no positional args supported for widgets" + kwargs = el.kwargs + resolved = None + # a value that can be an event listener (on_): a callable, or None + maybe_listener = False + for name, value in kwargs.items(): + t = type(value) + if t in _SCALAR_TYPES: + if value is None: + maybe_listener = True + continue + if t is list: + new_value = _mount_list(m, value, c, nodes, None if key is None else f"{key}{name}/") + elif isinstance(value, ElementBase): + new_value = _mount_node(m, value, c, nodes, None if key is None else f"{key}{name}/") + elif t is tuple or t is dict or isinstance(value, (list, tuple, dict)): + new_value = _mount_value(m, value, c, nodes, None if key is None else f"{key}{name}/") + else: + maybe_listener = True + continue + if resolved is None: + resolved = dict(kwargs) + resolved[name] = new_value + if m.failed: + # this pass will be undone, do not make more widgets + return None + rc = m.rc + if resolved is None: + # (no copy: the constructor gets the kwargs unpacked) + resolved = kwargs + element_class = type(el) + plain = _plain_classes.get(element_class, -1) + if plain == -1: + plain = _plain_class(element_class) + recording = m.recording + count = len(recording) + if plain != 0: + # Element._create_widget, with the recording of this mount + listeners = None + info = _widget_info(component) + traits = info.trait_names + name = None + if maybe_listener: + for name in resolved: + if name not in traits: + break + else: + name = None + if name is not None: + # a kwarg that is not a trait: an event listener (on_) + for name in list(resolved): + if name.startswith("on_") and name not in traits: + if resolved is kwargs: + resolved = dict(kwargs) + if listeners is None: + listeners = {} + listeners[name] = resolved.pop(name) + try: + widget = info.widget(**resolved) + except Exception: + # let reconciliation make it (and handle the exception) as it always does + m.failed = True + return None + widget_class = type(widget) + if info.batched is not widget_class: + if not getattr(widget_class.hold_trait_notifications, "_reacton_batched", False): + _core._install_batched_hold(widget_class) + info.batched = widget_class + widget._reacton_rc = rc + if el._meta: + widget._react_meta = dict(el._meta) + if listeners is None: + nodes.append(widget) + else: + if plain == 2: + # Element._add_widget_event_listener, inline (rc.context is c). The observers + # stay in the node (Element._callback_wrappers gets them when the dicts are + # made, see materialize; removing the node unobserves them) + added = None + for name, callback in listeners.items(): + if callback is not None: + listener = _Listener(rc, c, name, widget, callback) + widget.observe(listener, name[3:]) + # (one listener, or a list of them) + if added is None: + added = listener + elif type(added) is list: + added.append(listener) + else: + added = [added, listener] + nodes.append((el, widget, added)) + else: + for name, callback in listeners.items(): + if callback is not None: + el._add_widget_event_listener(widget, name, callback) # type: ignore[attr-defined] + nodes.append((el, widget)) + handlers = el._event_handlers + if handlers: + for handler in handlers: + handler._reacton_attach(widget) + orphan_ids = None + if len(recording) > count + 1 or (len(recording) == count + 1 and recording[count] is not widget): + widgets_dict = _core._get_widgets_dict() + orphan_ids = {w.model_id for w in recording[count:] if w is not widget and w.comm is not None and w.model_id in widgets_dict} + else: + try: + widget, orphan_ids = el._create_widget(dict(resolved)) # type: ignore[attr-defined] + except BaseException: + m.failed = True + return None + nodes.append((el, widget)) + if orphan_ids: + widgets_dict = _core._get_widgets_dict() + for orphan_widget in [widgets_dict[k] for k in orphan_ids]: + if _core._is_shared_ipyvue_template(orphan_widget): + orphan_ids.discard(orphan_widget.model_id) + if orphan_ids: + rc._orphans.setdefault(widget.model_id, set()).update(orphan_ids) + return widget + + +def _mount_list(m, value, c, nodes, dkey): + values = [] + index = 0 + for x in value: + if isinstance(x, ElementBase): + w = _mount_node(m, x, c, nodes, None if dkey is None else f"{dkey}{index}/") + if type(w) is _FragmentWidget: + values.extend(w.children) + else: + values.append(w) + elif type(x) in _SCALAR_TYPES: + values.append(x) + else: + w = _mount_value(m, x, c, nodes, None if dkey is None else f"{dkey}{index}/") + if type(w) is _FragmentWidget: + values.extend(w.children) + else: + values.append(w) + index += 1 + return values + + +def _mount_value(m, value, c, nodes, dkey): + # (as core._visit_children_values: lists, tuples and dicts become new plain ones) + t = type(value) + if t is list: + return _mount_list(m, value, c, nodes, dkey) + if t is tuple: + return tuple(_mount_list(m, value, c, nodes, dkey)) + if t is dict: + return {k: _mount_value(m, v, c, nodes, None if dkey is None else f"{dkey}{k}/") for k, v in value.items()} + if t in _SCALAR_TYPES: + return value + if isinstance(value, ElementBase): + return _mount_node(m, value, c, nodes, dkey) + if isinstance(value, (list, tuple)): + values = _mount_list(m, value, c, nodes, dkey) + return tuple(values) if isinstance(value, tuple) else values + if isinstance(value, dict): + return {k: _mount_value(m, v, c, nodes, None if dkey is None else f"{dkey}{k}/") for k, v in value.items()} + return value + + +# -------------------------------------------------------------------------------------------- +# after the mount: the effects (reconciliation), the dicts (materialize), the undo + + +def init_context(c): + """The containers every component context that renders in two phases uses (ComponentContext).""" + c.elements_next = {} + c.elements = {} + c.children_next = {} + c.children = {} + c.widgets = {} + c.element_to_widget = {} + c.effects = [] + c.memo = [] + c.used_keys = set() + c.exceptions_self = [] + c.exceptions_children = [] + # most components use state (solara: every component), and have a container widget: making + # these on first use (see ComponentContext.__getattr__) costs more than an empty dict + c.state = {} + c.setters = {} + c.resolved_kwargs = {} + + +def init_render_context(rc, element, container, children_trait, handle_error, fast): + """The fields of a new render context (see reacton.core._RenderContext), without its + initial state. fast: a _RenderContextFast.""" + rc.element = element + rc.container = container + rc.children_next_trait = children_trait + rc.first_render = True + rc.container_adders = [] + root = _new_instance(_ComponentContext) + root.parent = None + # (init_context without the hook containers: the root runs no component body) + root.elements_next = {} + root.elements = {} + root.children_next = {} + root.children = {} + root.widgets = {} + root.element_to_widget = {} + root.used_keys = set() + root.exceptions_self = [] + root.exceptions_children = [] + rc.context = root + rc.context_root = root + rc.render_count = 0 + rc.last_root_widget = None + rc._is_rendering = False + rc._rerender_needed = False + rc.thread_lock = threading.Lock() + rc._closing = False + rc.tracebacks = [] + rc.handle_error = handle_error + rc.reconsolidating = False + rc._walk_all = True + rc._shared_widgets = {} + rc._shared_elements_next = set() + rc._shared_elements = set() + rc._orphans = {} + rc._old_element_ids = set() + if fast: + rc._mount_roots = {} + rc._mount_tops = [] + rc._mount_failed = False + rc._body_adder = ContainerAdder(None, "children") + + +def _info_enabled(): + # _logger.isEnabledFor(logging.INFO) without its frame: logging keeps the answer in + # Logger._cache (the same dict, cleared when a level changes); Logger.disabled is not in it + enabled = _logger_cache.get(20) if _logger_cache is not None else None + if enabled is None: + return _logger.isEnabledFor(20) + return enabled and not _logger.disabled + + +def render_fixed(element, handle_error=True): + """reacton.core.render_fixed: render element, without a container widget.""" + if fast_selected(): + # (_RenderContextFast(element, handle_error=handle_error), without the __init__ frame) + rc = _RenderContextFast.__new__(_RenderContextFast) + init_render_context(rc, element, None, "children", handle_error, True) + widget = render_first(rc, element, None) + else: + rc = _core._RenderContext(element, handle_error=handle_error) + widget = rc.render(element) + local.last_rc = weakref.ref(rc) + return widget, rc + + +# (the class default of _RenderContext._rerender_needed_reasons: no reasons yet) +_NO_REASONS: tuple = () + + +def add_rerender_reason(rc, reason): + """rc._rerender_needed_reasons.append(reason); the deque is made by the first reason (making + it costs more than the rest of a render context, and most never need one).""" + reasons = rc._rerender_needed_reasons + if reasons is _NO_REASONS: + reasons = rc._rerender_needed_reasons = deque(maxlen=2) + reasons.append(reason) + + +def fast_selected(): + """REACTON_FAST=1 in the environment (read for every render: it can change at runtime).""" + if not cython.compiled: + return os.environ.get("REACTON_FAST") == "1" + # (os.environ.get is a few Python calls; os.environ writes go to the C environment too) + return _getenv_fast() # type: ignore[name-defined] # noqa: F821 (C, see _fastcore.pxd) + + +def render_first(rc, element, container): + """render() of a new fast render context (render() and render_fixed() of reacton.core). + + When the root element is a component element, the first pass mounts it and the + reconciliation finishes the mount, without the walks of render(). When something needs + more passes (state set or an exception during the mount, an effect that sets state), the + loop of render() takes over; in every other case render() does it all. + """ + root = rc.context_root + if ( + rc._is_rendering + or not rc.first_render + or not rc._mount_enabled + or DEBUG + or rc._closing + or not isinstance(element, ElementBase) + or element.is_shared + or (type(element.component) is not _ComponentFunction and isinstance(element.component, _ComponentWidget)) + # (state_set made contexts for the initial state) + or root.children_next + # (render() logs its phases) + or _info_enabled() + ): + return rc.render(element, container) + lock = rc.thread_lock + if not lock.acquire(False): + # (render() waits for it, or refuses a render from a component body) + return rc.render(element, container) + if container is None: + container = rc.container + widget = None + try: + prev_rc = getattr(local, "rc", None) + rc._lock_thread = threading.get_ident() + try: + local.rc = rc + rc.element = element + rc._rerender_needed = False + global _log_debug + _log_debug = False + rc.render_count += 1 + rc._is_rendering = True + root.exception_handler = False + root.root_element_next = element + # the first pass: _render and _render_component of a new component element + root.has_shared = False + root.child_order_counter = 1 + key = element._key + if key is None: + key = "/" + root.used_keys.add(key) + root.elements_next[key] = element + element._render_count += 1 + try: + widget = mount_component(rc, element, key, root, 0, None) + more = rc._rerender_needed or rc._mount_failed or root.exceptions_children + if more: + # (undoes the mount: the passes of render() follow) + rc._end_render_pass() + except BaseException: + rc._is_rendering = False + raise + rc.first_render = False + rc._walk_all = False + if more: + widget = rc._render_loop(container, None, False, False) + else: + rc._mount_tops = [] + # the reconciliation of the root: _reconsolidate of the mounted component + # (reconsolidating stays True when it raises, see below) + rc.reconsolidating = True + try: + finish_mount(rc, root.children_next[key]) + root.children[key] = root.children_next.pop(key) + widget = root.widgets[key] + finally: + root.elements[key] = root.elements_next.pop(key) + rc.reconsolidating = False + root.root_element = element + root.root_element_next = None + rc.last_root_widget = widget + if container: + container.children = [widget] + if rc._rerender_needed and not root.exceptions_children and not root.exceptions_self: + # an effect set state + widget = rc._render_loop(container, widget, False, False) + rc._is_rendering = False + except BaseException: + if rc.reconsolidating: + # (reconciliation raised halfway: the widgets it made need the normal removal) + rc.reconsolidating = False + else: + rc._discard_aborted_pass() + raise + finally: + local.rc = prev_rc + rc._is_rendering = False + rc._lock_thread = None + finally: + lock.release() + if root.exceptions_children or root.exceptions_self: + return rc._render_error() + return widget + + +def finish_mount(rc, root): + """Reconciliation of a mounted subtree (rc.context is the parent of root): run the effects, + children first, and hook the root widget into the parent.""" + parent_context = rc.context + # (the contexts of the mount that have effects, children first) + order = root.mount_order + raised = False + try: + for context in order: + effects = context.effects + parent = context.parent + rc.context = context + for effect in effects: + if effect.next is not None or effect.executed: + rc._process_effects(context, parent) + break + try: + effect._cleanup = effect.callable() + effect.executed = True + except BaseException as e: + _logger.exception("Effect %r raised exception %r", effect.callable, e) + parent.exceptions_self.append(e) + rc._set_rerender_needed("Exception ocurred during effect") + rc._mark_dirty(parent) + parent.needs_render = True + raised = True + if raised: + _bubble_exceptions(root) + widget = root.compact_widget + el = root.invoke_element + key = root.key_in_parent + if el._meta: + widget._react_meta = {**getattr(widget, "_react_meta", {}), **el._meta} + parent_context.widgets[key] = widget + parent_context.element_to_widget[el] = widget + finally: + rc.context = parent_context + root.mount_order = None + rc._mount_roots.pop(root, None) + + +def _bubble_exceptions(c): + # an effect of the mount raised: the exceptions go up to the top of the mount, children + # first (as in _render_component), through the contexts that do not handle them + for node in c.nodes: + if type(node) is _MountedContext: + _bubble_exceptions(node) + if c.exceptions_self or c.exceptions_children and not c.exception_handler: + parent = c.parent + parent.exceptions_children.extend(c.exceptions_self) + parent.exceptions_children.extend(c.exceptions_children) + + +def materialize(c): + """Make the dicts of a mounted context from its nodes (the same keys as the two phase walk). + + For the update paths, get_widget, state_get: the first time they use one of the dicts. + """ + nodes = c.nodes + c.nodes = None + w = _Materialize(c, nodes) + root = c.root_element + if root is not None: + w.node(root, "/") + c.elements = w.elements + c.widgets = w.widgets + c.children = w.children + c.element_to_widget = w.element_to_widget + c.used_keys = w.used_keys + c.resolved_kwargs = w.resolved_kwargs + c.elements_next = {} + c.children_next = {} + + +def partial_element_to_widget(c): + """element -> widget of a context that is still being mounted: the elements the walk made a + widget for so far (it goes in the same order). For _find_widget (use_event on an element + of a parent, whose widget exists already).""" + w = _Materialize(c, c.nodes, partial=True) + root = c.root_element + if root is not None: + try: + w.node(root, "/") + except _StopWalk: + pass + return w.element_to_widget + + +class _StopWalk(Exception): + pass + + +class _Materialize: + def __init__(self, c, nodes, partial=False): + self.partial = partial + self.nodes = nodes + self.index = 0 + self.order = 0 + self.elements = {} + self.widgets = {} + self.children = {} + self.element_to_widget = {} + self.used_keys = set() + self.resolved_kwargs = {} + + def node(self, el, default_key): + key = el._key + if key is None: + key = default_key + self.used_keys.add(key) + self.elements[key] = el + component = el.component + if not isinstance(component, _ComponentWidget): + if self.partial and (self.index >= len(self.nodes) or self.nodes[self.index].compact_widget is None): + raise _StopWalk() + child = self.nodes[self.index] + self.index += 1 + self.children[key] = child + child.key_in_parent = key + child.order_in_parent = self.order + self.order += 1 + widget = child.compact_widget + self.widgets[key] = widget + self.element_to_widget[el] = widget + return widget + resolved = None + start = self.index + for name, value in el.kwargs.items(): + if type(value) in _SCALAR_TYPES: + continue + new_value = self.value(value, f"{key}{name}/") + if resolved is None: + resolved = dict(el.kwargs) + resolved[name] = new_value + if self.partial and self.index >= len(self.nodes): + raise _StopWalk() + node = self.nodes[self.index] + self.index += 1 + if type(node) is tuple: + widget = node[1] + if len(node) == 3 and not self.partial: + # the observers the mount added: from now on removed as Element does it + callback_wrappers = _Element._callback_wrappers + added = node[2] + for listener in [added] if type(added) is _Listener else (added or ()): + callback_wrappers[(widget.model_id, listener.name, listener.callback)] = listener + else: + widget = node + self.widgets[key] = widget + self.element_to_widget[el] = widget + if self.index - 1 != start: + # elements in the kwargs (each one took a node): the kwargs the widget was made + # with, as the update path compares them (see _mount_node: without listeners) + assert resolved is not None + traits = _widget_info(component).trait_names + if not traits.issuperset(resolved): + for name in list(resolved): + if name.startswith("on_") and name not in traits: + del resolved[name] + self.resolved_kwargs[key] = resolved + return widget + + def value(self, value, key): + t = type(value) + if t is list or t is tuple or (t is not dict and isinstance(value, (list, tuple))): + values = [] + index = 0 + for x in value: + if type(x) in _SCALAR_TYPES: + values.append(x) + else: + w = self.node(x, f"{key}{index}/") if isinstance(x, ElementBase) else self.value(x, f"{key}{index}/") + if type(w) is _FragmentWidget: + values.extend(w.children) + else: + values.append(w) + index += 1 + return tuple(values) if isinstance(value, tuple) else values + if t is dict or isinstance(value, dict): + return {k: (x if type(x) in _SCALAR_TYPES else self.value(x, f"{key}{k}/")) for k, x in value.items()} + if isinstance(value, ElementBase): + return self.node(value, key) + return value + + +def undo_mounts(rc): + """Undo the mounts of a pass that cannot keep them (see the module comment): close their + widgets, and give their contexts the render bookkeeping of the two phase walk.""" + undo = _Undo(rc) + for top in rc._mount_tops: + if top.nodes is not None: + undo.component(top) + rc._mount_roots = {} + rc._mount_tops = [] + rc._mount_failed = False + + +class _Undo: + # the walk of the mount again (depth first, the same order, for the shared elements), with + # the keys of the two phase walk + def __init__(self, rc): + self.rc = rc + self.shared_seen = set() + + def component(self, c): + nodes = c.nodes + c.nodes = None + for node in nodes: + if not isinstance(node, _ComponentContext): + _close_widget_node(self.rc, node) + state = _UndoContext([node for node in nodes if isinstance(node, _ComponentContext)]) + root = c.root_element + if root is not None: + self.node(state, root, "/") + c.elements_next = state.elements_next + c.children_next = state.children_next + c.used_keys = state.used_keys + c.child_order_counter = state.order + c.root_element_next = root + c.root_element = None + c.elements = {} + c.children = {} + c.widgets = {} + c.element_to_widget = {} + c.resolved_kwargs = {} + c.compact_widget = None + c.mount_order = None + + def node(self, state, el, default_key): + key = el._key + if key is None: + key = default_key + state.used_keys.add(key) + if el.is_shared: + if el in self.shared_seen: + return + self.shared_seen.add(el) + state.elements_next[key] = el + if isinstance(el.component, _ComponentWidget): + for name, value in el.kwargs.items(): + if type(value) not in _SCALAR_TYPES: + self.value(state, value, f"{key}{name}/") + return + if el.is_shared and (el.args or el.kwargs): + self.value(state, el.kwargs, key) + self.value(state, el.args, key) + if state.index >= len(state.contexts): + # (the mount did not get this far) + return + child = state.contexts[state.index] + state.index += 1 + state.children_next[key] = child + child.key_in_parent = key + child.order_in_parent = state.order + state.order += 1 + if child.nodes is not None: + self.component(child) + + def value(self, state, value, key): + if isinstance(value, ElementBase): + self.node(state, value, key) + elif isinstance(value, (list, tuple)): + index = 0 + for x in value: + if type(x) not in _SCALAR_TYPES: + self.value(state, x, f"{key}{index}/") + index += 1 + elif isinstance(value, dict): + for k, x in value.items(): + if type(x) not in _SCALAR_TYPES: + self.value(state, x, f"{key}{k}/") + + +class _UndoContext: + def __init__(self, contexts): + self.contexts = contexts + self.index = 0 + self.order = 0 + self.elements_next = {} + self.children_next = {} + self.used_keys = set() + + +def _close_widget_node(rc, node): + # close the widget of a node (and the widgets it made as a side effect) + # (a node (el, widget, listeners): the mount added the observers, see _mount_node) + mounted_listeners = False + added = None + if type(node) is tuple: + el = node[0] + widget = node[1] + if len(node) == 3: + mounted_listeners = True + added = node[2] + else: + el = None + widget = node + orphans = rc._orphans.pop(widget.model_id, None) if rc._orphans else None + if orphans: + widgets_dict = _core._get_widgets_dict() + for orphan in orphans: + orphan_widget = widgets_dict.get(orphan) + if orphan_widget: + _core.close_widget(orphan_widget) + if mounted_listeners: + # (Element._cleanup_callbacks: the observers the mount added) + for listener in [added] if type(added) is _Listener else (added or ()): + try: + widget.unobserve(listener, listener.name[3:]) + except ValueError: + _logger.error("Could not remove event listener %r from %r", listener.name, widget) + el._close_widget(widget) + elif el is not None: + el._cleanup_callbacks(widget) + el._close_widget(widget) + else: + # Element._close_widget, inline + close = widget.close + if callable(close): + close() + else: + _core.close_widget(widget) # logs the warning + try: + del widget._reacton_rc + except AttributeError: + pass + + +def remove_mounted(rc, child_context, closing): + """Remove a mounted component (rc.context is its parent): the same order of effect + cleanups, handler removals and widget closes as reacton.core's _remove_element (or + _close_element when closing), from its nodes.""" + errors = _remove_mounted(rc, child_context, closing) + if errors: + rc.context.exceptions_children.extend(errors) + + +def _remove_mounted(rc, child_context, closing): + # Returns the exceptions that bubble up to the parent (None when there are none): the + # exceptions of the cleanups of this component, and those of its children when this + # component does not handle exceptions. (Local lists: most mounted components never have + # exceptions, and their contexts do not get the exception lists.) + context = rc.context + errors = None + errors_children = None + # (rc.context is only switched to child_context when an effect cleanup, a handler or a + # child component can use it: most mounted components have none) + switched = False + try: + effects = child_context.effects + if effects: + rc.context = child_context + switched = True + for effect in effects: + if not effect._cleaned_up: + cleanup = effect._cleanup + try: + if cleanup is not None: + cleanup() + except BaseException as e: + _logger.exception("Effect cleanup %r raised exception %r", effect.callable, e) + if errors is None: + errors = [] + errors.append(e) + if not closing: + rc._set_rerender_needed("Exception ocurred during effect") + rc._mark_dirty(child_context) + effect._cleaned_up = True + if not closing: + handlers = child_context.event_handlers + if handlers and not switched: + rc.context = child_context + switched = True + for handler in handlers: + try: + handler._reacton_detach() + except BaseException as e: + _logger.exception("Removing event handler %r raised exception %r", handler, e) + if errors is None: + errors = [] + errors.append(e) + rc._set_rerender_needed("Exception ocurred during effect") + rc._mark_dirty(child_context) + nodes = child_context.nodes + child_context.nodes = None + for node in nodes: + if type(node) is _MountedContext or (type(node) is not tuple and isinstance(node, _ComponentContext)): + if not switched: + rc.context = child_context + switched = True + if node.nodes is not None: + sub = _remove_mounted(rc, node, closing) + if sub: + if errors_children is None: + errors_children = [] + errors_children.extend(sub) + else: + # (its dicts were made, e.g. by get_widget): as the two phase walk does, + # which puts the exceptions that bubble up on rc.context (child_context) + sub = child_context.exceptions_children = [] + if closing: + rc._close_component_context(node) + else: + rc._remove_component_context(node) + if sub: + if errors_children is None: + errors_children = [] + errors_children.extend(sub) + else: + _close_widget_node(rc, node) + finally: + if switched: + rc.context = context + if errors is None: + if errors_children is not None and child_context.exception_handler: + # (a component that handles exceptions keeps those of its children) + return None + return errors_children + if errors_children is not None: + errors.extend(errors_children) + return errors + + +# ============================================================================================ +# The hooks. reacton.core's use_state, use_ref, use_memo, use_effect and reacton.ipyvue's +# use_event are these functions: for the fast renderer they do the work here (the same as the +# methods of reacton.core._RenderContext); any other render context gets its own methods. +# ============================================================================================ + +_FastRC: Any = None # reacton.core._RenderContextFast +_Ref: Any = None +_Effect: Any = None +# (annotated before _register_hooks declares it global: Python 3.7 rejects the other order) +_RerenderReason: Any = None +_logging_debug = 10 +# logging.DEBUG enabled for reacton (set at the start of every render, see core.render) +_log_debug = False +_STATE_KEYS = [str(i) for i in range(64)] +_VueWidget: Any = None + + +class RefBase: + """The data of reacton.core.Ref (use_ref).""" + + if not cython.compiled: + current: Any + + def __init__(self, initial_value): + self.current = initial_value + + +def _register_hooks(core): + global _FastRC, _Ref, _Effect, _RerenderReason + _FastRC = core._RenderContextFast + _Ref = core.Ref + _Effect = core.Effect + _RerenderReason = core.RerenderReason + + +def use_state(initial, key=None, eq=None): + rc = getattr(local, "rc", None) + if rc is None: + raise RuntimeError("No render context") + if type(rc) is not _FastRC: + return rc.use_state(initial, key, eq) + return rc_use_state(rc, initial, key, eq) + + +def rc_use_state(rc, initial, key, eq): + # reacton.core._RenderContext.use_state (both renderers) + context = rc.context + if key is None: + index = context.state_index + key = _STATE_KEYS[index] if index < 64 else str(index) + context.state_index = index + 1 + state = context.state + if state is None: + # (a mounted component: made on first use) + state = context.state = {} + context.setters = {} + if key not in state: + state[key] = initial + if type(initial) not in _SCALAR_TYPES: + if isinstance(initial, (list, dict, set)): + context.state_metadata[key] = len(initial) + elif utils.isinstance_lazy(initial, "pandas.DataFrame"): + context.state_metadata[key] = utils.dataframe_fingerprint(initial) + if _log_debug: + _logger.debug("Initial state = %r for key %r (%r)", initial, key, id(context)) + value = initial + else: + value = state[key] + if _log_debug: + _logger.debug("Got state = %r for key %r (%r)", value, key, id(context)) + # one setter per state key, made once (see core._RenderContext.use_state) + setters = context.setters + setter = setters.get(key) + if setter is None: + if type(rc) is _FastRC: + setter = _Setter(rc, context, key, eq) + else: + setter = rc.make_setter(key, context, eq) + setters[key] = setter + elif type(setter) is _Setter: + if setter.eq is not eq: + # the latest eq, as when a setter was made every render + setter.eq = eq + else: + eq_cell = setter._reacton_eq + if eq_cell[0] is not eq: + eq_cell[0] = eq + return value, setter + + +def use_ref(initial_value): + rc = getattr(local, "rc", None) + if rc is None: + raise RuntimeError("No render context") + if type(rc) is not _FastRC: + return rc.use_ref(initial_value) + return _use_ref(rc.context, initial_value) + + +def rc_use_ref(rc, initial_value): + # reacton.core._RenderContext.use_ref (both renderers) + return _use_ref(rc.context, initial_value) + + +def _use_ref(context, initial_value): + # use_memo(lambda: Ref(initial_value), []), without the closure and the extra calls + memo = context.memo + index = context.memo_index + if index < len(memo): + value, dependencies_previous = memo[index] + if type(dependencies_previous) is not list or dependencies_previous: + # not the memo of a use_ref (e.g. conditional hooks): what use_memo would do + if not utils.equals(dependencies_previous, []): + value = _Ref(initial_value) + memo[index] = (value, []) + else: + value = _Ref(initial_value) + if type(memo) is tuple: + # (a mounted component: made on first use) + memo = context.memo = [] + memo.append((value, [])) + context.memo_index = index + 1 + return value + + +def use_memo(f, dependencies=None, debug_name=None): + rc = getattr(local, "rc", None) + if rc is None: + raise RuntimeError("No render context") + if debug_name is None: + debug_name = f.__name__ + if dependencies is None: + import inspect + + dependencies = inspect.getclosurevars(f).nonlocals + dependencies = {k: v for k, v in dependencies.items() if not k.startswith("__")} + if type(rc) is not _FastRC: + return rc.use_memo(f, dependencies, debug_name) + return rc_use_memo(rc, f, dependencies, debug_name) + + +def rc_use_memo(rc, f, dependencies, debug_name): + # reacton.core._RenderContext.use_memo (both renderers) + context = rc.context + name = debug_name or "no-name" + memo = context.memo + index = context.memo_index + if len(memo) <= index: + value = f() + entry = (value, dependencies) + if type(memo) is tuple: + # (a mounted component: made on first use) + memo = context.memo = [] + memo.append(entry) + context.memo_index = index + 1 + if _log_debug: + _logger.debug("Initial memo = %r for index %r (debug-name: %r)", entry, index, name) + return value + entry = memo[index] + value, dependencies_previous = entry + if utils.equals(dependencies_previous, dependencies): + if _log_debug: + _logger.debug("Got memo hit = %r for index %r (debug-name: %r)", entry, index, name) + else: + if _log_debug: + _logger.debug("Replace memo with = %r for index %r (debug-name: %r)", entry, index, name) + value = f() + memo[index] = (value, dependencies) + context.memo_index = index + 1 + return value + + +def use_effect(effect, dependencies=None): + rc = getattr(local, "rc", None) + if rc is None: + raise RuntimeError("No render context") + if type(rc) is not _FastRC: + return rc.use_effect(effect, dependencies) + rc_use_effect(rc, effect, dependencies) + + +def _new_effect(callable, dependencies): + # reacton.core.Effect(callable, dependencies), without its __init__ frame + new = _new_instance(_Effect) + new.callable = callable + new.dependencies = dependencies + return new + + +def use_context(user_context): + """reacton.core.use_context (see there).""" + counter, set_counter = use_state(0) + rc = local.rc # (use_state checked there is one) + # the nearest ancestor that provides the context + context = rc.context.parent + value = None + while context is not None: + user_contexts = context.user_contexts + if user_context in user_contexts: + value = user_contexts.get(user_context) + break + context = context.parent + # listen to the provided value (provide() renders the listeners again) + use_effect(_ContextConnect(context, user_context, _ContextListener(set_counter)), [context]) + if context is None: + return user_context._default_value + return value + + +def _increment(x): + return x + 1 + + +class _ContextListener: + """use_context: render the component again (provide() calls it when the value changes).""" + + def __init__(self, set_counter): + self.set_counter = set_counter + + def __call__(self): + self.set_counter(_increment) + + +class _ContextConnect: + """The effect of use_context: add the listener to the providing context (and remove it).""" + + def __init__(self, context, user_context, listener): + self.context = context + self.user_context = user_context + self.listener = listener + + def __call__(self): + if self.context is not None: + self.context.context_listeners[self.user_context].add(self.listener) + return self.disconnect + + def disconnect(self): + self.context.context_listeners[self.user_context].remove(self.listener) + + +def rc_use_effect(rc, effect, dependencies): + # reacton.core._RenderContext.use_effect (both renderers) + context = rc.context + effects = context.effects + index = context.effect_index + context.effect_index = index + 1 + if len(effects) <= index: + if type(effects) is tuple: + # (a mounted component: made on first use) + effects = context.effects = [] + effects.append(_new_effect(effect, dependencies)) + if _log_debug: + _logger.debug("Initial effect = %r for index %r (%r)", effect, index, dependencies) + else: + previous_effect = effects[index] + if _log_debug: + _logger.debug("Setting next effect = %r for index %r (%r)", effect, index, dependencies) + if previous_effect.executed: + if dependencies is not None and utils.equals(previous_effect.dependencies, dependencies): + # the same dependencies: the effect does not run again + previous_effect.next = None + else: + # line up, reconciliation cleans up the previous one and runs this one + previous_effect.next = _new_effect(effect, dependencies) + else: + # replace + effects[index] = _new_effect(effect, dependencies) + + +class _EventHandler: + """The handler of one use_event hook, made once (like a stable setter). + + The renderer registers it on the widget of the element when that widget is created or + updated (Element._event_handlers), and removes it when the component of the hook goes + away. It calls the latest callback given to use_event. + """ + + def __init__(self, rc, context, event_and_modifiers, callback): + self.rc = rc + self.context = context + self.callback = callback + self.event = event_and_modifiers + self.widget = None + self.registered_event = None + # the hook is gone: never register again (an element can outlive the hook, e.g. a + # memoized element of a parent that gets a new widget later) + self.removed = False + + def __call__(self, *args): + try: + self.callback(*args) + except Exception as e: + # because widgets don't have a context, but are a child of a component + # we add it to exceptions_children, not exception_self + # this allows a component to catch the exception of a direct child + self.context.exceptions_children.append(e) + self.rc.force_update() + + def _reacton_attach(self, widget): + if self.removed: + return + event = self.event + previous = self.widget + if widget is previous and event == self.registered_event: + return + if previous is not None and previous.comm is not None and self.registered_event is not None: + previous.on_event(self.registered_event, self, remove=True) + widget.on_event(event, self) + self.widget = widget + self.registered_event = event + + def _reacton_detach(self): + self.removed = True + widget = self.widget + self.widget = None + if widget is None or self.rc._closing: + # the whole tree is going away: removing the handler would sync + # the _events trait to the frontend (one message per widget) + # right before the comm is closed anyway + return + if widget.comm is not None: + widget.on_event(self.registered_event, self, remove=True) + + +def _is_vue(component): + # the widget class of a ComponentWidget is an ipyvue widget (kept on the component) + vue = component._reacton_vue + if vue is None: + global _VueWidget + if _VueWidget is None: + import ipyvue + + _VueWidget = ipyvue.VueWidget + vue = component._reacton_vue = issubclass(component.widget, _VueWidget) + return vue + + +def use_event(el, event_and_modifiers, callback): + """reacton.ipyvue.use_event (see there).""" + rc = getattr(local, "rc", None) + if rc is None: + raise RuntimeError("No render context") + context = rc.context + if type(rc) is _FastRC: + ref = _use_ref(context, None) + else: + ref = rc.use_ref(None) + handler = ref.current + if handler is None: + handler = ref.current = _EventHandler(rc, context, event_and_modifiers, callback) + context.event_handlers = (*context.event_handlers, handler) + else: + handler.callback = callback + if event_and_modifiers != handler.event: + handler.event = event_and_modifiers + if handler.widget is not None: + handler._reacton_attach(handler.widget) + + # Put the event name in the widget constructor arguments: the synced _events + # trait then goes along with the comm open message. The later on_event call + # only updates _events when the event set differs, so this saves one update + # message per widget per event. When the element is reused from a previous + # render (memoized) and the widget already exists, on_event falls back to + # syncing _events itself. + component = el.component + if isinstance(component, _ComponentWidget) and _is_vue(component): + events = el.kwargs.get("_events") + if events is None: + el.kwargs["_events"] = [event_and_modifiers] + elif event_and_modifiers not in events: + # do not mutate the list, it could be shared with a previous element + el.kwargs["_events"] = [*events, event_and_modifiers] + + handlers = el._event_handlers + if handler not in handlers: + if el._render_count: + _core._add_event_handlers(el, (handler,), context, rc) + else: + el._event_handlers = (*handlers, handler) + + +class _Listener: + """The observer of an on_ event listener (Element._add_widget_event_listener). + + It calls the callback with the new value. An exception goes to the component that + rendered the element (a component can catch the exception of a direct child). + """ + + def __init__(self, rc, context, name, widget, callback): + self.rc = rc + self.context = context + self.name = name + self.widget = widget + self.callback = callback + + def __call__(self, change): + if getattr(local, "events_supressed", False): + return + _logger.info("event %r on %r with %r", self.name, self.widget, change) + try: + self.callback(change["new"]) + except Exception as e: + # because widgets don't have a context, but are a child of a component + # we add it to exceptions_children, not exception_self + self.context.exceptions_children.append(e) + self.rc.force_update() + + +class _Setter: + """The setter of one use_state key of the fast renderer, made once per key (like React's + setState). The same as the set_ closure of reacton.core._RenderContext.make_setter.""" + + def __init__(self, rc, context, key, eq): + # NOTE: rc and context are held strongly, and that is a requirement: a setter may be + # the ONLY reference keeping its component context alive. close() empties the + # contexts to break the cycles. + self.rc = rc + self.context = context + self.key = key + # (the latest eq, updated by use_state) + self.eq = eq + self.created_stack = None + if DEBUG: + import traceback + + self.created_stack = traceback.format_stack() + + def __call__(self, value): + rc = self.rc + if rc._closing: + # the render context is closed (or closing) and the tree is (being) torn down: + # nothing to update. (After close, context.state is empty.) + return + context = self.context + key = self.key + eq = self.eq + state = context.state + if callable(value): + value = value(state[key]) + if _logger.isEnabledFor(20): + _logger.info("Set state = %r for key %r (previous value was %r) (%r)", value, key, state[key], id(rc.context)) + should_update = False + new_metadata = None + if eq is None: + previous = state[key] + if previous is value and isinstance(value, (list, dict, set)): + new_metadata = len(value) + if context.state_metadata[key] != new_metadata: + _warn_mutated( + "You are setting the state with the same object, this will usually not trigger a rerender. " + f"The length of {value} changed compared to the previous time it was set. Are you mutating an existing state object? " + "A common mistake is appending to a list, mutating a dict or set, etc." + ) + should_update = True + if previous is value and utils.isinstance_lazy(value, "pandas.DataFrame"): + new_metadata = utils.dataframe_fingerprint(value) + if context.state_metadata[key] != new_metadata: + _warn_mutated( + "You are setting the state with the dataframe, this will usually not trigger a rerender. " + "We noticed the ids of the dataframe series are changed. Are you mutating an dataframe? " + "Consider making a copy of the dataframe." + ) + should_update = True + equals = eq or utils.equals + should_update = not equals(state[key], value) or should_update + if should_update: + prev_value = state[key] + state[key] = value + if isinstance(value, (list, dict, set)) and new_metadata is None: + new_metadata = len(value) + if new_metadata is None and utils.isinstance_lazy(value, "pandas.DataFrame"): + new_metadata = utils.dataframe_fingerprint(value) + context.state_metadata[key] = new_metadata + context.needs_render = True + rc._mark_dirty(context) + if rc._rerender_needed is False: + if DEBUG: + import traceback + + reason = _RerenderReason( + reason=f"state changed with key {key}", + prev_value=prev_value, + next_value=value, + created_stack=self.created_stack, + trigger_stack=traceback.format_stack(), + ) + else: + reason = _RerenderReason(reason=f"state changed with key {key}", prev_value=prev_value, next_value=value) + add_rerender_reason(rc, reason) + rc._rerender_needed = True + rc._possible_rerender() + + +def _warn_mutated(message): + import warnings + + # (point at the code that called the setter: a compiled function has no frame of its own) + warnings.warn(message, UserWarning, stacklevel=2 if cython.compiled else 3) diff --git a/reacton/_fastcore_import.py b/reacton/_fastcore_import.py new file mode 100644 index 0000000..a9ee3d3 --- /dev/null +++ b/reacton/_fastcore_import.py @@ -0,0 +1,31 @@ +"""Import reacton._fastcore: compiled when it was built with Cython, else the plain Python. + +REACTON_CYTHON=0 in the environment forces the plain Python version, also when a compiled +module is there (to test both, or to debug). +""" + +import importlib.util +import os +import sys + + +def _load(): + if os.environ.get("REACTON_CYTHON", "1") == "0": + name = __package__ + "._fastcore" + existing = sys.modules.get(name) + if existing is not None: + return existing + path = os.path.join(os.path.dirname(__file__), "_fastcore.py") + spec = importlib.util.spec_from_file_location(name, path) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + sys.modules[name] = module + spec.loader.exec_module(module) + return module + from . import _fastcore as module + + return module + + +_fastcore = _load() +compiled: bool = not _fastcore.__file__.endswith(".py") diff --git a/reacton/bqplot.py b/reacton/bqplot.py index 91e747f..ff6d34f 100644 --- a/reacton/bqplot.py +++ b/reacton/bqplot.py @@ -18,7 +18,7 @@ class FigureElement(Element[bqplot.Figure]): def __enter__(self): rc = _get_render_context() - ca = ContainerAdder[bqplot.Figure](self, "marks") + ca = ContainerAdder(self, "marks") rc.container_adders.append(ca) return self @@ -89,12 +89,11 @@ def _Albers( @implements(_Albers) def Albers(**kwargs): - widget_cls = bqplot.scales.Albers - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_Albers_component, kwargs=kwargs) del _Albers +_Albers_component = reacton.core.ComponentWidget(widget=bqplot.scales.Albers) def _AlbersUSA( @@ -128,12 +127,11 @@ def _AlbersUSA( @implements(_AlbersUSA) def AlbersUSA(**kwargs): - widget_cls = bqplot.scales.AlbersUSA - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_AlbersUSA_component, kwargs=kwargs) del _AlbersUSA +_AlbersUSA_component = reacton.core.ComponentWidget(widget=bqplot.scales.AlbersUSA) def _Axis( @@ -232,12 +230,11 @@ def _Axis( @implements(_Axis) def Axis(**kwargs): - widget_cls = bqplot.axes.Axis - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_Axis_component, kwargs=kwargs) del _Axis +_Axis_component = reacton.core.ComponentWidget(widget=bqplot.axes.Axis) def _Bars( @@ -398,12 +395,11 @@ def _Bars( @implements(_Bars) def Bars(**kwargs): - widget_cls = bqplot.marks.Bars - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_Bars_component, kwargs=kwargs) del _Bars +_Bars_component = reacton.core.ComponentWidget(widget=bqplot.marks.Bars) def _BaseAxis() -> Element[bqplot.axes.BaseAxis]: @@ -413,12 +409,11 @@ def _BaseAxis() -> Element[bqplot.axes.BaseAxis]: @implements(_BaseAxis) def BaseAxis(**kwargs): - widget_cls = bqplot.axes.BaseAxis - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_BaseAxis_component, kwargs=kwargs) del _BaseAxis +_BaseAxis_component = reacton.core.ComponentWidget(widget=bqplot.axes.BaseAxis) def _Bins( @@ -563,12 +558,11 @@ def _Bins( @implements(_Bins) def Bins(**kwargs): - widget_cls = bqplot.marks.Bins - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_Bins_component, kwargs=kwargs) del _Bins +_Bins_component = reacton.core.ComponentWidget(widget=bqplot.marks.Bins) def _Boxplot( @@ -652,12 +646,11 @@ def _Boxplot( @implements(_Boxplot) def Boxplot(**kwargs): - widget_cls = bqplot.marks.Boxplot - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_Boxplot_component, kwargs=kwargs) del _Boxplot +_Boxplot_component = reacton.core.ComponentWidget(widget=bqplot.marks.Boxplot) def _ColorAxis( @@ -712,12 +705,11 @@ def _ColorAxis( @implements(_ColorAxis) def ColorAxis(**kwargs): - widget_cls = bqplot.axes.ColorAxis - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_ColorAxis_component, kwargs=kwargs) del _ColorAxis +_ColorAxis_component = reacton.core.ComponentWidget(widget=bqplot.axes.ColorAxis) def _ColorScale( @@ -772,12 +764,11 @@ def _ColorScale( @implements(_ColorScale) def ColorScale(**kwargs): - widget_cls = bqplot.scales.ColorScale - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_ColorScale_component, kwargs=kwargs) del _ColorScale +_ColorScale_component = reacton.core.ComponentWidget(widget=bqplot.scales.ColorScale) def _DOMWidget( @@ -792,12 +783,11 @@ def _DOMWidget( def DOMWidget(**kwargs): if isinstance(kwargs.get("layout"), dict): kwargs["layout"] = Layout(**kwargs["layout"]) - widget_cls = ipywidgets.widgets.domwidget.DOMWidget - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_DOMWidget_component, kwargs=kwargs) del _DOMWidget +_DOMWidget_component = reacton.core.ComponentWidget(widget=ipywidgets.widgets.domwidget.DOMWidget) def _DateColorScale( @@ -845,12 +835,11 @@ def _DateColorScale( @implements(_DateColorScale) def DateColorScale(**kwargs): - widget_cls = bqplot.scales.DateColorScale - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_DateColorScale_component, kwargs=kwargs) del _DateColorScale +_DateColorScale_component = reacton.core.ComponentWidget(widget=bqplot.scales.DateColorScale) def _DateScale( @@ -888,12 +877,11 @@ def _DateScale( @implements(_DateScale) def DateScale(**kwargs): - widget_cls = bqplot.scales.DateScale - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_DateScale_component, kwargs=kwargs) del _DateScale +_DateScale_component = reacton.core.ComponentWidget(widget=bqplot.scales.DateScale) def _EquiRectangular( @@ -924,12 +912,11 @@ def _EquiRectangular( @implements(_EquiRectangular) def EquiRectangular(**kwargs): - widget_cls = bqplot.scales.EquiRectangular - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_EquiRectangular_component, kwargs=kwargs) del _EquiRectangular +_EquiRectangular_component = reacton.core.ComponentWidget(widget=bqplot.scales.EquiRectangular) def _Figure( @@ -1065,12 +1052,11 @@ def _Figure( def Figure(**kwargs): if isinstance(kwargs.get("layout"), dict): kwargs["layout"] = w.Layout(**kwargs["layout"]) - widget_cls = bqplot.figure.Figure - comp = reacton.core.ComponentWidget(widget=widget_cls) - return FigureElement(comp, kwargs=kwargs) + return FigureElement(_Figure_component, kwargs=kwargs) del _Figure +_Figure_component = reacton.core.ComponentWidget(widget=bqplot.figure.Figure) def _FlexLine( @@ -1155,12 +1141,11 @@ def _FlexLine( @implements(_FlexLine) def FlexLine(**kwargs): - widget_cls = bqplot.marks.FlexLine - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_FlexLine_component, kwargs=kwargs) del _FlexLine +_FlexLine_component = reacton.core.ComponentWidget(widget=bqplot.marks.FlexLine) def _GeoScale( @@ -1181,12 +1166,11 @@ def _GeoScale( @implements(_GeoScale) def GeoScale(**kwargs): - widget_cls = bqplot.scales.GeoScale - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_GeoScale_component, kwargs=kwargs) del _GeoScale +_GeoScale_component = reacton.core.ComponentWidget(widget=bqplot.scales.GeoScale) def _Gnomonic( @@ -1226,12 +1210,11 @@ def _Gnomonic( @implements(_Gnomonic) def Gnomonic(**kwargs): - widget_cls = bqplot.scales.Gnomonic - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_Gnomonic_component, kwargs=kwargs) del _Gnomonic +_Gnomonic_component = reacton.core.ComponentWidget(widget=bqplot.scales.Gnomonic) def _Graph( @@ -1350,12 +1333,11 @@ def _Graph( @implements(_Graph) def Graph(**kwargs): - widget_cls = bqplot.marks.Graph - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_Graph_component, kwargs=kwargs) del _Graph +_Graph_component = reacton.core.ComponentWidget(widget=bqplot.marks.Graph) def _GridHeatMap( @@ -1483,12 +1465,11 @@ def _GridHeatMap( @implements(_GridHeatMap) def GridHeatMap(**kwargs): - widget_cls = bqplot.marks.GridHeatMap - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_GridHeatMap_component, kwargs=kwargs) del _GridHeatMap +_GridHeatMap_component = reacton.core.ComponentWidget(widget=bqplot.marks.GridHeatMap) def _HeatMap( @@ -1561,12 +1542,11 @@ def _HeatMap( @implements(_HeatMap) def HeatMap(**kwargs): - widget_cls = bqplot.marks.HeatMap - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_HeatMap_component, kwargs=kwargs) del _HeatMap +_HeatMap_component = reacton.core.ComponentWidget(widget=bqplot.marks.HeatMap) def _Hist( @@ -1668,12 +1648,11 @@ def _Hist( @implements(_Hist) def Hist(**kwargs): - widget_cls = bqplot.marks.Hist - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_Hist_component, kwargs=kwargs) del _Hist +_Hist_component = reacton.core.ComponentWidget(widget=bqplot.marks.Hist) def _Image( @@ -1741,12 +1720,11 @@ def _Image( @implements(_Image) def Image(**kwargs): - widget_cls = bqplot.marks.Image - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_Image_component, kwargs=kwargs) del _Image +_Image_component = reacton.core.ComponentWidget(widget=bqplot.marks.Image) def _Interaction() -> Element[bqplot.interacts.Interaction]: @@ -1773,12 +1751,11 @@ def _Interaction() -> Element[bqplot.interacts.Interaction]: @implements(_Interaction) def Interaction(**kwargs): - widget_cls = bqplot.interacts.Interaction - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_Interaction_component, kwargs=kwargs) del _Interaction +_Interaction_component = reacton.core.ComponentWidget(widget=bqplot.interacts.Interaction) def _Label( @@ -1926,12 +1903,11 @@ def _Label( @implements(_Label) def Label(**kwargs): - widget_cls = bqplot.marks.Label - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_Label_component, kwargs=kwargs) del _Label +_Label_component = reacton.core.ComponentWidget(widget=bqplot.marks.Label) def _LinearScale( @@ -1991,12 +1967,11 @@ def _LinearScale( @implements(_LinearScale) def LinearScale(**kwargs): - widget_cls = bqplot.scales.LinearScale - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_LinearScale_component, kwargs=kwargs) del _LinearScale +_LinearScale_component = reacton.core.ComponentWidget(widget=bqplot.scales.LinearScale) def _Lines( @@ -2142,12 +2117,11 @@ def _Lines( @implements(_Lines) def Lines(**kwargs): - widget_cls = bqplot.marks.Lines - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_Lines_component, kwargs=kwargs) del _Lines +_Lines_component = reacton.core.ComponentWidget(widget=bqplot.marks.Lines) def _LogScale( @@ -2183,12 +2157,11 @@ def _LogScale( @implements(_LogScale) def LogScale(**kwargs): - widget_cls = bqplot.scales.LogScale - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_LogScale_component, kwargs=kwargs) del _LogScale +_LogScale_component = reacton.core.ComponentWidget(widget=bqplot.scales.LogScale) def _Map( @@ -2272,12 +2245,11 @@ def _Map( @implements(_Map) def Map(**kwargs): - widget_cls = bqplot.marks.Map - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_Map_component, kwargs=kwargs) del _Map +_Map_component = reacton.core.ComponentWidget(widget=bqplot.marks.Map) def _Mark( @@ -2396,12 +2368,11 @@ def _Mark( @implements(_Mark) def Mark(**kwargs): - widget_cls = bqplot.marks.Mark - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_Mark_component, kwargs=kwargs) del _Mark +_Mark_component = reacton.core.ComponentWidget(widget=bqplot.marks.Mark) def _Mercator( @@ -2442,12 +2413,11 @@ def _Mercator( @implements(_Mercator) def Mercator(**kwargs): - widget_cls = bqplot.scales.Mercator - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_Mercator_component, kwargs=kwargs) del _Mercator +_Mercator_component = reacton.core.ComponentWidget(widget=bqplot.scales.Mercator) def _OHLC( @@ -2545,12 +2515,11 @@ def _OHLC( @implements(_OHLC) def OHLC(**kwargs): - widget_cls = bqplot.marks.OHLC - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_OHLC_component, kwargs=kwargs) del _OHLC +_OHLC_component = reacton.core.ComponentWidget(widget=bqplot.marks.OHLC) def _OrdinalColorScale( @@ -2596,12 +2565,11 @@ def _OrdinalColorScale( @implements(_OrdinalColorScale) def OrdinalColorScale(**kwargs): - widget_cls = bqplot.scales.OrdinalColorScale - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_OrdinalColorScale_component, kwargs=kwargs) del _OrdinalColorScale +_OrdinalColorScale_component = reacton.core.ComponentWidget(widget=bqplot.scales.OrdinalColorScale) def _OrdinalScale( @@ -2633,12 +2601,11 @@ def _OrdinalScale( @implements(_OrdinalScale) def OrdinalScale(**kwargs): - widget_cls = bqplot.scales.OrdinalScale - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_OrdinalScale_component, kwargs=kwargs) del _OrdinalScale +_OrdinalScale_component = reacton.core.ComponentWidget(widget=bqplot.scales.OrdinalScale) def _Orthographic( @@ -2683,12 +2650,11 @@ def _Orthographic( @implements(_Orthographic) def Orthographic(**kwargs): - widget_cls = bqplot.scales.Orthographic - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_Orthographic_component, kwargs=kwargs) del _Orthographic +_Orthographic_component = reacton.core.ComponentWidget(widget=bqplot.scales.Orthographic) def _PanZoom( @@ -2719,12 +2685,11 @@ def _PanZoom( @implements(_PanZoom) def PanZoom(**kwargs): - widget_cls = bqplot.interacts.PanZoom - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_PanZoom_component, kwargs=kwargs) del _PanZoom +_PanZoom_component = reacton.core.ComponentWidget(widget=bqplot.interacts.PanZoom) def _Pie( @@ -2859,12 +2824,11 @@ def _Pie( @implements(_Pie) def Pie(**kwargs): - widget_cls = bqplot.marks.Pie - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_Pie_component, kwargs=kwargs) del _Pie +_Pie_component = reacton.core.ComponentWidget(widget=bqplot.marks.Pie) def _Scale( @@ -2900,12 +2864,11 @@ def _Scale( @implements(_Scale) def Scale(**kwargs): - widget_cls = bqplot.scales.Scale - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_Scale_component, kwargs=kwargs) del _Scale +_Scale_component = reacton.core.ComponentWidget(widget=bqplot.scales.Scale) def _Scatter( @@ -3113,12 +3076,11 @@ def _Scatter( @implements(_Scatter) def Scatter(**kwargs): - widget_cls = bqplot.marks.Scatter - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_Scatter_component, kwargs=kwargs) del _Scatter +_Scatter_component = reacton.core.ComponentWidget(widget=bqplot.marks.Scatter) def _ScatterGL( @@ -3227,12 +3189,11 @@ def _ScatterGL( @implements(_ScatterGL) def ScatterGL(**kwargs): - widget_cls = bqplot.marks.ScatterGL - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_ScatterGL_component, kwargs=kwargs) del _ScatterGL +_ScatterGL_component = reacton.core.ComponentWidget(widget=bqplot.marks.ScatterGL) def _Stereographic( @@ -3277,12 +3238,11 @@ def _Stereographic( @implements(_Stereographic) def Stereographic(**kwargs): - widget_cls = bqplot.scales.Stereographic - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_Stereographic_component, kwargs=kwargs) del _Stereographic +_Stereographic_component = reacton.core.ComponentWidget(widget=bqplot.scales.Stereographic) def _Toolbar( @@ -3328,12 +3288,11 @@ def _Toolbar( def Toolbar(**kwargs): if isinstance(kwargs.get("layout"), dict): kwargs["layout"] = w.Layout(**kwargs["layout"]) - widget_cls = bqplot.toolbar.Toolbar - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_Toolbar_component, kwargs=kwargs) del _Toolbar +_Toolbar_component = reacton.core.ComponentWidget(widget=bqplot.toolbar.Toolbar) def _Tooltip( @@ -3376,9 +3335,8 @@ def _Tooltip( def Tooltip(**kwargs): if isinstance(kwargs.get("layout"), dict): kwargs["layout"] = w.Layout(**kwargs["layout"]) - widget_cls = bqplot.default_tooltip.Tooltip - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_Tooltip_component, kwargs=kwargs) del _Tooltip +_Tooltip_component = reacton.core.ComponentWidget(widget=bqplot.default_tooltip.Tooltip) diff --git a/reacton/core.py b/reacton/core.py index 2f6342b..58f8bfc 100644 --- a/reacton/core.py +++ b/reacton/core.py @@ -7,7 +7,6 @@ """ -import collections import contextlib import copy import functools @@ -17,6 +16,7 @@ import sys import threading import traceback +import types import typing import weakref from collections import defaultdict @@ -52,6 +52,20 @@ import reacton.patch_display # noqa: F401 # has sidefx from . import _version, patch, utils # noqa: F401 +from ._fastcore_import import _fastcore + +# the hot building blocks (compiled when reacton was built with Cython, see _fastcore.py) +if typing.TYPE_CHECKING: + from ._fastcore import ContainerAdder, ElementBase, RefBase, ValueElementBase, find_elements, local, mime_bundle_default, widget_render_error_msg +else: + RefBase = _fastcore.RefBase + ContainerAdder = _fastcore.ContainerAdder + ElementBase = _fastcore.ElementBase + ValueElementBase = _fastcore.ValueElementBase + find_elements = _fastcore.find_elements + local = _fastcore.local + widget_render_error_msg = _fastcore.widget_render_error_msg + mime_bundle_default = _fastcore.mime_bundle_default __version__ = _version.__version__ @@ -180,7 +194,6 @@ def hold_trait_notifications(self, *args, **kwargs): _last_rc = None # used for testing -local = threading.local() T = TypeVar("T") U = TypeVar("U") W = TypeVar("W") # used for widgets @@ -202,13 +215,6 @@ def hold_trait_notifications(self, *args, **kwargs): MIME_WIDGETS = "application/vnd.jupyter.widget-view+json" -widget_render_error_msg = ( - """Cannot show widget. You probably want to rerun the code cell above (Click in the code cell, and press Shift+Enter ⇧+↩).""" -) - -mime_bundle_default = {"text/plain": "Cannot show ipywidgets in text", "text/html": widget_render_error_msg} - - def element(cls, **kwargs): return ComponentWidget(cls)(**kwargs) @@ -370,118 +376,15 @@ def __call__(self, *args, **kwargs) -> Union[widgets.Widget, "Element"]: pass -class Element(Generic[W]): +class Element(ElementBase, Generic[W]): + # (the data of an element, and the methods the renderers call for every element, are in + # _fastcore.ElementBase: component, args, kwargs, _key, is_shared, _meta, _render_count, + # key(), meta(), shared(), _arguments_changed()) child_prop_name = "children" # to make every unique on_value callback to a unique wrapper # so that we can remove the listeners _callback_wrappers: Dict[Tuple[str, str, Callable], Callable] = {} create_lock: ContextManager = threading.Lock() - # a plain attribute (not a property): it is read for every element in every walk - is_shared: bool = False - # Defaults as class attributes: every component body makes elements, most of them - # never change these. (_meta is never changed in place, meta() makes a new dict.) - mime_bundle: Dict[str, Any] = mime_bundle_default - _key: Optional[str] = None - _meta: Dict[str, Any] = {} - # for debugging/testing only - _render_count = 0 - _key_frozen: bool = False - # facts about the kwargs of a widget element, learned when its widget is created or - # updated (None: not known), so a close of the whole tree can skip work - _on_kwargs: Optional[bool] = None # a kwarg starts with on_ (maybe an event listener) - _leaf: Optional[bool] = None # no elements in the kwargs - # handlers (of reacton.ipyvue.use_event) to register on the widget of this element when it - # is created or updated: objects with _reacton_attach(widget) - _event_handlers: Tuple[Any, ...] = () - - def __init__(self, component, args=None, kwargs=None): - self.component = component - self.args = args or [] - self.kwargs = kwargs or {} - - rc = getattr(local, "rc", None) - if rc is not None: - container_adders = rc.container_adders - if container_adders: - adder = container_adders[-1] - if type(adder) is ContainerAdder: - adder.created.append(self) - else: - adder.add(self) - if DEBUG: - # since we construct widgets or components from a different code path - # we want to preserve the original call stack, by manually tracking frames - try: - assert False - except AssertionError: - self.traceback = cast(TracebackType, sys.exc_info()[2]) - - assert self.traceback is not None - assert self.traceback.tb_frame is not None - assert self.traceback.tb_frame.f_back is not None - frame_py = self.traceback.tb_frame.f_back.f_back - assert frame_py is not None - self.traceback = TracebackType(tb_frame=frame_py, tb_lasti=self.traceback.tb_lasti, tb_lineno=frame_py.f_lineno, tb_next=None) - - def _arguments_changed(self, other: "Element"): - # called for every child of a component that renders again: the same objects - # (small ints, interned strings, the same callbacks) need no utils.equals call - args = self.args - other_args = other.args - kwargs = self.kwargs - other_kwargs = other.kwargs - if args: - if len(args) != len(other_args): - return True - elif other_args: - return True - if kwargs: - if len(kwargs) != len(other_kwargs): - return True - for k, v in kwargs.items(): - if k not in other_kwargs: - return True - other_v = other_kwargs[k] - if v is not other_v and not utils.equals(v, other_v): - return True - elif other_kwargs: - return True - if args: - for a, b in zip(args, other_args): - if a is not b and not utils.equals(a, b): - return True - return False - - def key(self, value: str): - """Returns the same element with a custom key set. - - This can help render performance. See documentation for details. - """ - if self._key_frozen: - raise RuntimeError("Element keys should not be mutated after rendering") - self._key = value - return self - - def meta(self, **kwargs): - """Add metadata to the created widget. - - This can be used to find a widget for testing. - """ - self._meta = {**self._meta, **kwargs} - return self - - # the old name of is_shared - @property - def _shared(self) -> bool: - return self.is_shared - - @_shared.setter - def _shared(self, value: bool): - self.is_shared = value - - def shared(self): - self.is_shared = True - return self def __repr__(self): def format_arg(value): @@ -535,7 +438,7 @@ def _ipython_display_(self, **kwargs): def __enter__(self): rc = _get_render_context() - ca = ContainerAdder[W](self, "children") + ca = ContainerAdder(self, "children") assert rc.context is not None rc.container_adders.append(ca) return self @@ -668,18 +571,15 @@ def _update_widget_event_listener(self, widget: widgets.Widget, name: str, callb self._add_widget_event_listener(widget, name, callback) def _add_widget_event_listener(self, widget: widgets.Widget, name: str, callback: Callable): - target_name = name[3:] - callback_exception_safe = _event_handler_exception_wrapper(callback) - - def on_change(change): - if are_events_supressed(): - return - logger.info("event %r on %r with %r", name, widget, change) - callback_exception_safe(change["new"]) - + # the observer calls the callback with the new value (exceptions go to the component + # that renders this element, see _fastcore._Listener) + rc = get_render_context() + context = rc.context + assert context is not None + on_change = _fastcore._Listener(rc, context, name, widget, callback) key = (widget.model_id, name, callback) self._callback_wrappers[key] = on_change - widget.observe(on_change, target_name) + widget.observe(on_change, name[3:]) def _remove_widget_event_listener(self, widget: widgets.Widget, name: str, callback: Callable): target_name = name[3:] @@ -707,10 +607,8 @@ def get(self) -> V: ... def set(self, value: V): ... -class ValueElement(Generic[W, V], Element[W]): - def __init__(self, value_property, component, args=None, kwargs=None): - self.value_property = value_property - Element.__init__(self, component, args, kwargs) # type: ignore[arg-type] # (faster than super()) +class ValueElement(ValueElementBase, Element[W], Generic[W, V]): + # (the value_property and __init__ are in _fastcore.ValueElementBase) # TODO: we want to enable something like this, but requires a good hash function # for the key @@ -744,70 +642,30 @@ def connect(self, value: Value[V]): FuncT = TypeVar("FuncT", bound=Callable[..., Element]) -def find_elements(value: Union[Element, List, Tuple, Dict]) -> Set[Element]: - if isinstance(value, Element): - el = value - elements = {el} - if not isinstance(el.kwargs, dict): - raise RuntimeError("keyword arguments for {el} should be a dict, not {el.kwargs}") - elements |= find_elements(el.args) - elements |= find_elements(el.kwargs) - return elements - elif isinstance(value, (tuple, list)): - elements = set() - for child in value: - if isinstance(child, (Element, tuple, list, dict)): - elements |= find_elements(child) - return elements - elif isinstance(value, dict): - elements = set() - for child in value.values(): - if isinstance(child, (Element, tuple, list, dict)): - elements |= find_elements(child) - return elements - - # the list of a ContainerAdder that is not on the stack (never appended to) -_NO_ELEMENTS: List["Element"] = [] - - -class ContainerAdder(Generic[W]): - def __init__(self, el: Element[W], prop_name: str): - self.el = el - self.prop_name = prop_name - self.created: List[Element] = [] - - def add(self, el): - self.created.append(el) - - def collect(self): - children = set() - for el in self.created: - children |= find_elements(el) - {el} - top_level = [k for k in self.created if k not in children] - return top_level +_NO_ELEMENTS: List["Element"] = _fastcore._NO_ELEMENTS class ComponentWidget(Component): mime_bundle: Dict[str, Any] = mime_bundle_default widget: Type[widgets.Widget] + # (for the mount: what it keeps about the widget class, see _fastcore._WidgetInfo) + _reacton_info: Any = None + # (for use_event: the widget class is an ipyvue widget) + _reacton_vue: Optional[bool] = None - def __new__(cls, widget: Type[widgets.Widget], mime_bundle=mime_bundle_default): - # The generated element factories make one per element. One instance per widget - # class is cheaper, and makes the component compares of the render walks identity - # checks (__eq__ compares the widget classes). - if cls is ComponentWidget and mime_bundle is mime_bundle_default: - self = _component_widgets.get(widget) - if self is not None: - return self - self = super().__new__(cls) - if mime_bundle is not mime_bundle_default: - self.mime_bundle = mime_bundle - self.widget = widget - self.name = widget.__name__ - if cls is ComponentWidget and mime_bundle is mime_bundle_default: - _component_widgets[widget] = self - return self + # The generated element factories make one per element. One instance per widget class is + # cheaper, and makes the component compares of the render walks identity checks (__eq__ + # compares the widget classes). It is kept on the widget class itself: a class attribute + # read is much cheaper than a WeakValueDictionary lookup, and the class -> instance -> + # class cycle is freed by gc like any class (a class made at runtime, e.g. by a hot reload, + # is not kept alive). (see _fastcore.component_widget_new) + if typing.TYPE_CHECKING: + + def __new__(cls, widget: Type[widgets.Widget], mime_bundle=mime_bundle_default): ... + + else: + __new__ = staticmethod(_fastcore.component_widget_new) def __eq__(self, rhs): if self is rhs: @@ -819,6 +677,13 @@ def __eq__(self, rhs): def __repr__(self): return f"Component[{self.widget!r}]" + def __getnewargs__(self): + # (pickle/copy: __new__ needs the widget class; the default mime bundle is compared by + # identity, so it is not passed on) + if self.mime_bundle is mime_bundle_default: + return (self.widget,) + return (self.widget, self.mime_bundle) + def __call__(self, *args, **kwargs): el: Element = Element(self, args, kwargs) # TODO: temporary, we cannot change the constructor @@ -828,12 +693,6 @@ def __call__(self, *args, **kwargs): return el -# ComponentWidget per widget class (see ComponentWidget.__new__). Weak values: an entry goes -# away when no element uses it any more, so widget classes made at runtime (e.g. by a hot -# reload) can be freed. -_component_widgets: "weakref.WeakValueDictionary[type, ComponentWidget]" = weakref.WeakValueDictionary() - - class ComponentFunction(Component): def __init__(self, f: Callable[[], Element], mime_bundle=mime_bundle_default, value_name=None): self.f = f @@ -918,14 +777,8 @@ def __eq__(self, rhs): def __repr__(self): return f"react.component({self.f.__module__}.{self.f.__name__})" - def __call__(self, *args, **kwargs): - if self.value_name is not None: - el: Element = ValueElement(self.value_name, self, args, kwargs) - else: - el = Element(self, args, kwargs) - if self.mime_bundle is not mime_bundle_default: - el.mime_bundle = self.mime_bundle - return el + # make an element of this component (see _fastcore.component_call) + __call__ = _fastcore.component_call @overload @@ -1017,7 +870,7 @@ def get_widget(el: Element): else: if el in context.element_to_widget: return context.element_to_widget[el] - if id(el) in rc._old_element_ids or el._key_frozen: + if id(el) in rc._old_element_ids or el._render_count: # (the fast renderer does not record the ids: every rendered element is frozen) raise KeyError(f"Element {el} was found to be in a previous render, you may have used a stale element") raise KeyError(f"Element {el} not found in all known widgets") # for the component {context.widgets}") @@ -1033,7 +886,7 @@ def _add_event_handlers(el: Element, handlers: Tuple[Any, ...], context: "Compon if handler in current: continue current = el._event_handlers = (*current, handler) - if el._key_frozen: + if el._render_count: # The element was rendered before, so its widget may exist already and not be created # or updated again. E.g. a memoized element, or an element of a parent that a child # hooks into (the fast mount makes the parent's widget before the child renders). @@ -1207,9 +1060,9 @@ def clear(): return exception, clear -class Ref(Generic[T]): - def __init__(self, initial_value: T): - self.current = initial_value +class Ref(RefBase, Generic[T]): + # (current, and __init__(initial_value), are in _fastcore.RefBase) + current: T def use_ref(initial_value: T) -> Ref[T]: @@ -1229,7 +1082,12 @@ def provide(self, obj: T): context = rc.context assert context is not None prev = context.user_contexts_prev.get(self, self._default_value) - context.user_contexts[self] = obj + user_contexts = context.user_contexts + if user_contexts is _fastcore._EMPTY: + # (a mounted component shares one empty dict until it provides something) + user_contexts = context.user_contexts = {} + user_contexts[self] = obj + _fastcore._provides += 1 if not utils.equals(prev, obj): for listener in context.context_listeners.get(self, []): listener() @@ -1435,13 +1293,16 @@ class ComponentContext: partial: bool = False # the element tree of this component holds shared elements (they need the full walk) has_shared: bool = False - # the render phase of the fast renderer also created the widgets of this new component - # (the fused mount): reconciliation only has to finish it, see _finish_mount - mounted: bool = False + # A component mounted by the fast renderer (see _fastcore): its element tree in the order + # its widgets were made (children first), until the dicts are made from it + nodes: Optional[List[Any]] = None # the root widget of a mounted component - mount_widget: Optional[ipywidgets.Widget] = None - # on the first component of a mounted subtree: all mounted components in it, children first + compact_widget: Optional[ipywidgets.Widget] = None + # on the first component of a mounted subtree: its components, children first (the + # effects run in that order when reconciliation finishes the mount) mount_order: Optional[List["ComponentContext"]] = None + # during a mount: the contexts that state_set made for its children (restored state) + precreated_children: Optional[Dict[str, "ComponentContext"]] = None def __init__( self, @@ -1456,22 +1317,7 @@ def __init__( if context_managers is not None: self.context_managers = context_managers # the containers every rendered component uses - self.elements_next = {} - self.elements = {} - self.children_next = {} - self.children = {} - self.widgets = {} - self.element_to_widget = {} - self.effects = [] - self.memo = [] - self.used_keys = set() - self.exceptions_self = [] - self.exceptions_children = [] - # most components use state (solara: every component), and have a container widget: - # making these on first use (see __getattr__) costs more than an empty dict - self.state = {} - self.setters = {} - self.resolved_kwargs = {} + _fastcore.init_context(self) # the dataclass constructor took every field as a keyword argument for name, value in fields.items(): if name not in _COMPONENT_CONTEXT_FIELDS: @@ -1513,8 +1359,51 @@ def __getattr__(self, name): "exceptions_children": list, } _COMPONENT_CONTEXT_FIELDS = set(ComponentContext.__annotations__) + + +class _MountedContext(ComponentContext): + """A component mounted by the fast renderer in one walk (see _fastcore). + + Only what the mount needs is set. The dicts of the update paths (_fastcore._MATERIALIZED) + are made from its nodes when they are first used; the hook containers when a hook needs + them. The class defaults are the state after a first render. + + The containers made later are slots: reading one that is not set calls __getattr__ (which + makes them), and the instance dict keeps fewer names (CPython shares the key table of + instance dicts for up to 30 names per class; more makes every attribute access slower). + """ + + __slots__ = _fastcore._MOUNTED_SLOTS + needs_render = False + needs_render_descendant = False + state: Dict = None # type: ignore[assignment] # made by use_state + setters: Dict[str, Callable] = None # type: ignore[assignment] + memo: List[Any] = () # type: ignore[assignment] # made by use_memo/use_ref + effects: List["Effect"] = () # type: ignore[assignment] # made by use_effect + # one shared empty dict until provide() makes one (see UserContext.provide) + user_contexts = _fastcore._EMPTY + user_contexts_prev = _fastcore._EMPTY + + if not typing.TYPE_CHECKING: + + def __getattr__(self, name): + if name in _fastcore._MATERIALIZED: + if self.nodes is not None: + if self.compact_widget is None: + # still being mounted (or not kept): only look up what is there already + if name == "element_to_widget": + return _fastcore.partial_element_to_widget(self) + return {} if name != "used_keys" else set() + _fastcore.materialize(self) + return getattr(self, name) + elif name == "nodes" or name == "compact_widget": + # (not set: torn down, see _teardown_component_context) + return None + return ComponentContext.__getattr__(self, name) + + # use_state keys of the first 64 hooks of a component (str(index)) -_STATE_KEYS = [str(i) for i in range(64)] +_STATE_KEYS = _fastcore._STATE_KEYS TEffect = TypeVar("TEffect", bound="Effect") @@ -1540,6 +1429,11 @@ def _teardown_component_context(context: ComponentContext): # (parent, invoke_element, root_element... are None), and every container is made # again, empty, when it is used (see ComponentContext.__getattr__). context.__dict__.clear() + if type(context) is _MountedContext: + for name in _fastcore._MOUNTED_SLOTS: + # (set first: deleting an unset slot raises) + setattr(context, name, None) + delattr(context, name) class RerenderReason: @@ -1594,62 +1488,72 @@ def __call__(self): self.executed = True +# (makes the batch counter of a render context, once) +_batch_counter_lock = threading.Lock() + + class _RenderContext: context: Optional[ComponentContext] = None + # the ident of the thread that holds thread_lock (render) + _lock_thread: Optional[int] = None + + # the fields (set by _fastcore.init_render_context) + element: Element + container: Optional[widgets.Widget] + children_next_trait: str + first_render: bool + container_adders: List[ContainerAdder] + context_root: ComponentContext + render_count: int + last_root_widget: widgets.Widget + _is_rendering: bool + _rerender_needed: bool + # (only the last two reasons are read: the "too many renders" error message; made by the + # first one, see _add_rerender_reason) + _rerender_needed_reasons: Deque[RerenderReason] = _fastcore._NO_REASONS # type: ignore[assignment] + thread_lock: Any + _closing: bool + tracebacks: List[TracebackType] + handle_error: bool + reconsolidating: bool + # (made by the first batch, see __enter__) + _batch_counter: Optional[utils.ThreadSafeCounter] = None + # when set, the next render phase walks the whole tree instead of skipping subtrees in + # which no state changed (see _render) + _walk_all: bool + # shared elements outlive their component context: their widgets are kept here + _shared_widgets: Dict[Element, widgets.Widget] + # the shared elements a render pass processed (one widget per element), and after + # reconciliation + _shared_elements_next: Set[Element] + _shared_elements: Set[Element] + # widgets created as side effect (like Layout and Style), by the model id of the widget + # that made them (not all widgets are hashable, like plotly), to close them + _orphans: Dict[str, Set[str]] + # for detecting stale elements used get_widget + _old_element_ids: Set[int] def __init__(self, element: Element, container: widgets.Widget = None, children_trait="children", handle_error: bool = True, initial_state=None): - self.element = element - self.container = container - self.children_next_trait = children_trait - self.first_render = True - self.container_adders: List[ContainerAdder] = [] - self.context = ComponentContext() - self.context_root = self.context - self.render_count = 0 - self._lock_thread = cast(Optional[threading.Thread], None) - self.last_root_widget: widgets.Widget = None - self._is_rendering = False - self._rerender_needed = False - # only the last two reasons are ever read (the "too many renders" error message), - # and a reason holds the previous and next state value: keeping all of them kept - # every old state value alive until close() - self._rerender_needed_reasons: Deque[RerenderReason] = collections.deque(maxlen=2) - self.thread_lock = threading.Lock() - self._closing = False - self.tracebacks: List[TracebackType] = [] - self.handle_error = handle_error - self.reconsolidating = False - self._batch_counter = utils.ThreadSafeCounter() - # when set, the next render phase walks the whole tree instead of - # skipping subtrees in which no state changed (see _render) - self._walk_all = True + _fastcore.init_render_context(self, element, container, children_trait, handle_error, False) if initial_state: self.state_set(self.context_root, initial_state) - # element that are shared outlive the ComponentContext, so we - # store them in the RenderContext - self._shared_widgets: Dict[Element, widgets.Widget] = {} - - # each render phase, we track which (shared) elements we proccessed - # so we don't render them twice (only 1 widget per element) - self._shared_elements_next: Set[Element] = set() - - # once reconcilidated, shared elements move here. - self._shared_elements: Set[Element] = set() - - # widgets created as side effect (like Layout and Style) - # key is the widget model id (because some widgets are not hashable, like plotly) - # We keep track of this to make sure we clean up all widgets. - self._orphans: Dict[str, Set[str]] = {} - # for detecting stale elements used get_widget - self._old_element_ids: Set[int] = set() + def _add_rerender_reason(self, reason: RerenderReason): + _fastcore.add_rerender_reason(self, reason) def __enter__(self): - counter = self._batch_counter.increment() + batch_counter = self._batch_counter + if batch_counter is None: + with _batch_counter_lock: + batch_counter = self._batch_counter + if batch_counter is None: + batch_counter = self._batch_counter = utils.ThreadSafeCounter() + counter = batch_counter.increment() if counter == 1: logger.info("entering batch render") def __exit__(self, exc_type, exc_value, traceback): + assert self._batch_counter is not None counter = self._batch_counter.decrement() if counter == 0: logger.info("finishing batch render (%s)", "needs rerender" if self._rerender_needed else "no rerender needed") @@ -1685,6 +1589,10 @@ def close(self): while index < len(all_contexts): context = all_contexts[index] index += 1 + if context.nodes is not None: + # (a mounted component whose dicts were never made) + all_contexts.extend(node for node in context.nodes if isinstance(node, ComponentContext)) + continue all_contexts.extend(context.children.values()) if context.children_next: all_contexts.extend(context.children_next.values()) @@ -1726,6 +1634,10 @@ def state_get(self, context: Optional[ComponentContext] = None): if context is None: context = self.context_root data = {} + if context.state is None: + # (a mounted component without state: state_get hands out the live dict) + context.state = {} + context.setters = {} data["state"] = context.state if context.children: children_state = data["children"] = {} @@ -1739,84 +1651,18 @@ def state_set(self, context: ComponentContext, state): context.children_next[name] = ComponentContext(parent=context) self.state_set(context.children_next[name], state) + # the hooks (the same for both renderers, see _fastcore: compiled when reacton was built + # with Cython). The setter is made by make_setter (a closure here; the fast renderer has + # its own setter object). + def use_memo(self, f, dependencies, debug_name: str = None, use_nonlocals=False): - assert self.context is not None - name = debug_name or "no-name" - if len(self.context.memo) <= self.context.memo_index: - value = f() - memo = (value, dependencies) - self.context.memo.append(memo) - self.context.memo_index += 1 - if logger.isEnabledFor(logging.DEBUG): - logger.debug("Initial memo = %r for index %r (debug-name: %r)", memo, self.context.memo_index - 1, name) - return value - else: - memo = self.context.memo[self.context.memo_index] - value, dependencies_previous = memo - if utils.equals(dependencies_previous, dependencies): - if logger.isEnabledFor(logging.DEBUG): - logger.debug("Got memo hit = %r for index %r (debug-name: %r)", memo, self.context.memo_index, name) - else: - if logger.isEnabledFor(logging.DEBUG): - logger.debug("Replace memo with = %r for index %r (debug-name: %r)", memo, self.context.memo_index, name) - value = f() - memo = (value, dependencies) - self.context.memo[self.context.memo_index] = memo - self.context.memo_index += 1 - return value + return _fastcore.rc_use_memo(self, f, dependencies, debug_name) def use_ref(self, initial_value): - # use_memo(lambda: Ref(initial_value), []), without the closure and the extra calls - context = self.context - assert context is not None - memo = context.memo - index = context.memo_index - if index < len(memo): - value, dependencies_previous = memo[index] - if type(dependencies_previous) is not list or dependencies_previous: - # not the memo of a use_ref (e.g. conditional hooks): what use_memo would do - if not utils.equals(dependencies_previous, []): - value = Ref(initial_value) - memo[index] = (value, []) - else: - value = Ref(initial_value) - memo.append((value, [])) - context.memo_index = index + 1 - return value + return _fastcore.rc_use_ref(self, initial_value) def use_state(self, initial, key: str = None, eq: Callable[[Any, Any], bool] = None) -> Tuple[T, Callable[[Union[T, Callable[[T], T]]], None]]: - assert self.context is not None - if key is None: - index = self.context.state_index - key = _STATE_KEYS[index] if index < 64 else str(index) - self.context.state_index = index + 1 - if key not in self.context.state: - self.context.state[key] = initial - if isinstance(initial, (list, dict, set)): - self.context.state_metadata[key] = len(initial) - elif utils.isinstance_lazy(initial, "pandas.DataFrame"): - self.context.state_metadata[key] = utils.dataframe_fingerprint(initial) - if logger.isEnabledFor(logging.DEBUG): - logger.debug("Initial state = %r for key %r (%r)", initial, key, id(self.context)) - state = initial - else: - state = self.context.state[key] - if logger.isEnabledFor(logging.DEBUG): - logger.debug("Got state = %r for key %r (%r)", state, key, id(self.context)) - # the setter is made once per state key and kept (like React's setState): it compares - # equal to itself, so a child that gets it as an argument, or an effect that has it as - # a dependency, sees no change (utils.equals already found the setters of two renders - # equal, by comparing their code and closures) - setters = self.context.setters - setter = setters.get(key) - if setter is None: - setter = setters[key] = self.make_setter(key, self.context, eq) - else: - eq_cell = setter._reacton_eq # type: ignore - if eq_cell[0] is not eq: - # the latest eq, as when a setter was made every render - eq_cell[0] = eq - return state, setter + return _fastcore.rc_use_state(self, initial, key, eq) def make_setter(self, key, context: ComponentContext, eq: Callable[[Any, Any], bool] = None): if DEBUG: @@ -1884,7 +1730,7 @@ def set_(value): if DEBUG: trigger_stack = traceback.format_stack() - self._rerender_needed_reasons.append( + self._add_rerender_reason( RerenderReason( reason=f"state changed with key {key}", prev_value=prev_value, @@ -1894,7 +1740,7 @@ def set_(value): ) ) else: - self._rerender_needed_reasons.append(RerenderReason(reason=f"state changed with key {key}", prev_value=prev_value, next_value=value)) + self._add_rerender_reason(RerenderReason(reason=f"state changed with key {key}", prev_value=prev_value, next_value=value)) self._rerender_needed = True self._possible_rerender() @@ -1916,43 +1762,20 @@ def force_update(self): self.render(self.element, self.container) def use_effect(self, effect: EffectCallable, dependencies=None): - context = self.context - assert context is not None - effects = context.effects - index = context.effect_index - context.effect_index = index + 1 - if len(effects) <= index: - effects.append(Effect(effect, dependencies)) - if logger.isEnabledFor(logging.DEBUG): - logger.debug("Initial effect = %r for index %r (%r)", effect, index, dependencies) - else: - previous_effect = effects[index] - if logger.isEnabledFor(logging.DEBUG): - logger.debug("Setting next effect = %r for index %r (%r)", effect, index, dependencies) - if previous_effect.executed: - if dependencies is not None and utils.equals(previous_effect.dependencies, dependencies): - # The same dependencies: the effect does not run again. Reconciliation made - # this same compare on a new Effect (and dropped it); now no Effect is made - # (and one from an earlier render pass of this call is dropped). - previous_effect.next = None - else: - # line up, reconciliation cleans up the previous one and runs this one - previous_effect.next = Effect(effect, dependencies) - else: - # replace - effects[index] = Effect(effect, dependencies) + _fastcore.rc_use_effect(self, effect, dependencies) def update(self, element: Element): self._walk_all = True if self._is_rendering: self.element = element - self._rerender_needed_reasons.append(RerenderReason(reason="root element changed")) + self._add_rerender_reason(RerenderReason(reason="root element changed")) self._rerender_needed = True else: self.render(element, self.container) def _possible_rerender(self): - if not self._is_rendering and self._batch_counter.current() == 0: + batch_counter = self._batch_counter + if not self._is_rendering and (batch_counter is None or batch_counter.current() == 0): self.render(self.element, self.container) elif logger.isEnabledFor(logging.INFO): logger.info("No render phase triggered, already rendering") @@ -1993,16 +1816,17 @@ def render(self, element: Element, container: widgets.Widget = None): container = self.container was_locked = False if self.thread_lock.locked(): - if self._lock_thread == threading.current_thread(): + if self._lock_thread == threading.get_ident(): raise RuntimeError("Recursive render detected (avoided deadlock), current thread: %r" % threading.current_thread()) logger.info( - "Render phase still in progress, waiting for mutex to release (locked obtained by %r, we are in thread %r)", + "Render phase still in progress, waiting for mutex to release (locked by thread %r, we are in thread %r)", self._lock_thread, - threading.current_thread(), + threading.get_ident(), ) was_locked = True with self.thread_lock: - self._lock_thread = threading.current_thread() + # (the ident of the thread: threading.current_thread() is a Python call) + self._lock_thread = threading.get_ident() if was_locked: logger.info("Mutex released, continuing render phase") if self._closing or self.context is None: @@ -2012,8 +1836,6 @@ def render(self, element: Element, container: widgets.Widget = None): logger.info("Render requested on a closing/closed render context, ignoring") return container prev_rc = getattr(local, "rc", None) - # an exception that escapes while this is True aborted a render pass (see the except below) - in_render_phase = True try: local.rc = self self.element = element @@ -2024,6 +1846,8 @@ def render(self, element: Element, container: widgets.Widget = None): # the logging calls below cost a noticeable part of a small update when logging is off log_info = logger.isEnabledFor(logging.INFO) log_debug = logger.isEnabledFor(logging.DEBUG) + # (the hooks of the fast renderer check this, see _fastcore) + _fastcore._log_debug = log_debug if log_info: logger.info("Render phase: %r %r of %r", self.render_count, "main" if main_render_phase else "(nested)", self.element) self.render_count += 1 @@ -2048,111 +1872,7 @@ def render(self, element: Element, container: widgets.Widget = None): raise if main_render_phase: - stable = False - render_counts = 0 - while not stable and not self.context_root.exceptions_children: - # we started the rendering loop (main_render_phase is True), so we keep going - # but if an exception bubbled up, we should stop - while self._rerender_needed and not self.context_root.exceptions_children: - if render_counts > 50: - - def format(reason: RerenderReason): - f = f"Reason: {reason.reason}\nValue changed from {reason.prev_value} to {reason.next_value}\n" - if reason.created_stack: - f += f"Created at: {''.join(reason.created_stack)}\n" - if reason.trigger_stack: - f += f"Triggered at: {''.join(reason.trigger_stack)}\n" - return f - - self._rerender_needed_reasons[-1] - msg = f"Too many renders triggered, your render loop does not stop\nLast reason: {format(self._rerender_needed_reasons[-1])}\n" - if len(self._rerender_needed_reasons) >= 2: - msg += f"Previous reasons: {format(self._rerender_needed_reasons[-2])}\n" - raise RuntimeError(msg) - if log_info: - logger.info("Entering nested render phase: %r", self._rerender_needed_reasons[-1]) - self._rerender_needed = False - self._shared_elements_next = set() - self.context.exception_handler = False - self.context.exceptions_children = [] - self.context.exceptions_self = [] - - self._render(self.element, "/", parent_key=ROOT_KEY) - self._end_render_pass() - self._walk_all = False - if log_info: - logger.info("Render done: %r %r", self._rerender_needed, self._rerender_needed_reasons[-1]) - assert self.context is self.context_root - render_counts += 1 - if log_debug: - logger.debug("Render phase resulted in (next) elements:") - for el in self._shared_elements_next: - logger.debug("\t%r %x", el, id(el)) - - logger.debug("Current elements:") - for el in self._shared_elements: - logger.debug("\t %r %x", el, id(el)) - if self.context_root.exceptions_children: - # an exception bubbled up render - break - - if log_info: - logger.info("Render reconsolidate...") - self.reconsolidating = True - in_render_phase = False - try: - widget = self._reconsolidate(self.element, default_key="/", parent_key=ROOT_KEY) - finally: - self.reconsolidating = False - in_render_phase = True - if log_info: - logger.info("Render reconsolidate done") - self.context.root_element = self.context.root_element_next - self.context.root_element_next = None - - # remove stale elements of the root context itself - # (child contexts are swept during their reconciliation) - self._remove_stale_root_elements(ROOT_KEY) - - if self._shared_elements_next: - raise RuntimeError(f"Element not reconsolidated: {self._shared_elements_next}") - if log_debug: - logger.debug("Reconsolidate phase resulted in elements:") - for el in self._shared_elements: - logger.debug("\t%r %x", el, id(el)) - # RESET - assert self.context is self.context_root - if self.element.is_shared: - assert widget in self._shared_widgets.values() - else: - assert widget in self.context_root.widgets.values() - if self.last_root_widget is None: - self.last_root_widget = widget - else: - if container is None: - if self.last_root_widget != widget: - raise ValueError( - "You are not using a container, and the root component returned a new widget," - "make sure your root component always returns the same component type" - ) - if container: - if widget is None: - # Exception occurred, and we cannot render the widget - container.children = [] - else: - container.children = [widget] - - if self.context_root.exceptions_children or self.context_root.exceptions_self: - # an exception bubbled up during reconsolidate - break - - if self._rerender_needed: - if log_info: - logger.info("Need rerender after reconsolidation: %r", self._rerender_needed_reasons[-1]) - stable = False - else: - stable = True - + widget = self._render_loop(container, widget, log_info, log_debug) self._is_rendering = False self.context = context_prev if log_info: @@ -2165,7 +1885,10 @@ def format(reason: RerenderReason): # committed elements, but runs the effects the aborted pass chained, closed # over elements that were never reconciled (get_widget then fails with # "found in a previous render"). - if in_render_phase: + if self.reconsolidating: + # (reconciliation raised halfway: the widgets it made need the normal removal) + self.reconsolidating = False + else: self._discard_aborted_pass() if DEBUG: # construct a fake traceback (showing how the elements were constructed) @@ -2185,26 +1908,135 @@ def format(reason: RerenderReason): self._lock_thread = None assert self.context is self.context_root - exceptions = [*self.context.exceptions_children, *self.context_root.exceptions_self] - if exceptions: - exc = exceptions[0] - if DEBUG: - exc = _with_tracebacks(exc, self.tracebacks) + if self.context_root.exceptions_children or self.context_root.exceptions_self: + return self._render_error() + return widget + + def _render_error(self): + # the end of render() when a component raised (and no component handled it) + exc = [*self.context_root.exceptions_children, *self.context_root.exceptions_self][0] + if DEBUG: + exc = _with_tracebacks(exc, self.tracebacks) + + if self.handle_error: + logger.info("Exception occurred, rendering error message") + if exc.__traceback__ is None: + value = "Exception occurred, but no traceback available" + else: + error = "".join(traceback.format_exception(None, exc, exc.__traceback__)) + import html + + value = html.escape(error) + from . import ipywidgets as w + + return self.render(w.HTML(value="
" + value + "
", layout=w.Layout(overflow="auto")), self.container) + else: + raise exc + + def _render_loop(self, container, widget, log_info, log_debug): + # the passes and reconciliations of render() after its first pass, until stable + stable = False + render_counts = 0 + while not stable and not self.context_root.exceptions_children: + # we started the rendering loop (main_render_phase is True), so we keep going + # but if an exception bubbled up, we should stop + while self._rerender_needed and not self.context_root.exceptions_children: + if render_counts > 50: + + def format(reason: RerenderReason): + f = f"Reason: {reason.reason}\nValue changed from {reason.prev_value} to {reason.next_value}\n" + if reason.created_stack: + f += f"Created at: {''.join(reason.created_stack)}\n" + if reason.trigger_stack: + f += f"Triggered at: {''.join(reason.trigger_stack)}\n" + return f + + self._rerender_needed_reasons[-1] + msg = f"Too many renders triggered, your render loop does not stop\nLast reason: {format(self._rerender_needed_reasons[-1])}\n" + if len(self._rerender_needed_reasons) >= 2: + msg += f"Previous reasons: {format(self._rerender_needed_reasons[-2])}\n" + raise RuntimeError(msg) + if log_info: + logger.info("Entering nested render phase: %r", self._rerender_needed_reasons[-1]) + self._rerender_needed = False + self._shared_elements_next = set() + self.context_root.exception_handler = False + self.context_root.exceptions_children = [] + self.context_root.exceptions_self = [] + + self._render(self.element, "/", parent_key=ROOT_KEY) + self._end_render_pass() + self._walk_all = False + if log_info: + logger.info("Render done: %r %r", self._rerender_needed, self._rerender_needed_reasons[-1]) + assert self.context is self.context_root + render_counts += 1 + if log_debug: + logger.debug("Render phase resulted in (next) elements:") + for el in self._shared_elements_next: + logger.debug("\t%r %x", el, id(el)) + + logger.debug("Current elements:") + for el in self._shared_elements: + logger.debug("\t %r %x", el, id(el)) + if self.context_root.exceptions_children: + # an exception bubbled up render + break - if self.handle_error: - logger.info("Exception occurred, rendering error message") - if exc.__traceback__ is None: - value = "Exception occurred, but no traceback available" + if log_info: + logger.info("Render reconsolidate...") + # (reconsolidating stays True when it raises, see render()) + self.reconsolidating = True + widget = self._reconsolidate(self.element, default_key="/", parent_key=ROOT_KEY) + self.reconsolidating = False + if log_info: + logger.info("Render reconsolidate done") + self.context_root.root_element = self.context_root.root_element_next + self.context_root.root_element_next = None + + # remove stale elements of the root context itself + # (child contexts are swept during their reconciliation) + self._remove_stale_root_elements(ROOT_KEY) + + if self._shared_elements_next: + raise RuntimeError(f"Element not reconsolidated: {self._shared_elements_next}") + if log_debug: + logger.debug("Reconsolidate phase resulted in elements:") + for el in self._shared_elements: + logger.debug("\t%r %x", el, id(el)) + # RESET + assert self.context is self.context_root + if self.element.is_shared: + assert widget in self._shared_widgets.values() + else: + assert widget in self.context_root.widgets.values() + if self.last_root_widget is None: + self.last_root_widget = widget + else: + if container is None: + if self.last_root_widget != widget: + raise ValueError( + "You are not using a container, and the root component returned a new widget," + "make sure your root component always returns the same component type" + ) + if container: + if widget is None: + # Exception occurred, and we cannot render the widget + container.children = [] else: - error = "".join(traceback.format_exception(None, exc, exc.__traceback__)) - import html + container.children = [widget] - value = html.escape(error) - from . import ipywidgets as w + if self.context_root.exceptions_children or self.context_root.exceptions_self: + # an exception bubbled up during reconsolidate + break - return self.render(w.HTML(value="
" + value + "
", layout=w.Layout(overflow="auto")), self.container) + if self._rerender_needed: + if log_info: + logger.info("Need rerender after reconsolidation: %r", self._rerender_needed_reasons[-1]) + stable = False else: - raise exc + stable = True + return widget def _render(self, element: Element, default_key: str, parent_key: str): @@ -2225,7 +2057,6 @@ def _render(self, element: Element, default_key: str, parent_key: str): key = el._key if key is None: key = default_key - el._key_frozen = True logger.debug("Render: (%s,%s) - %r", parent_key, key, element) @@ -2246,7 +2077,7 @@ def _render(self, element: Element, default_key: str, parent_key: str): if el_prev is None: el_prev = context.elements.get(key) context.elements_next[key] = el - # used for testing + # used for testing, and it freezes the key (see Element._key_frozen) el._render_count += 1 if isinstance(el.component, ComponentWidget): @@ -2367,7 +2198,7 @@ def _render(self, element: Element, default_key: str, parent_key: str): self.tracebacks.append(el.traceback) logger.exception("Component %r raised exception %r", el.component, e) context.exceptions_self.append(e) - self._rerender_needed_reasons.append(RerenderReason(reason="Exception ocurred during render")) + self._add_rerender_reason(RerenderReason(reason="Exception ocurred during render")) self._rerender_needed = True context.needs_render = True @@ -2406,7 +2237,7 @@ def _render(self, element: Element, default_key: str, parent_key: str): except RuntimeError as e: logger.exception("Exception in hook count check") context.exceptions_self.append(e) - self._rerender_needed_reasons.append(RerenderReason(reason="Exception ocurred during render (hook count check)")) + self._add_rerender_reason(RerenderReason(reason="Exception ocurred during render (hook count check)")) self._rerender_needed = True context.needs_render = True # only expose to parent when no error occurs @@ -2428,7 +2259,7 @@ def _render(self, element: Element, default_key: str, parent_key: str): # this happens when an exception was added from an event handler # this means no exception was raised during the render phase # but we still need to rerender, until someone catches the exception - self._rerender_needed_reasons.append(RerenderReason(reason="Exception ocurred during render")) + self._add_rerender_reason(RerenderReason(reason="Exception ocurred during render")) self._rerender_needed = True self.context.needs_render = True @@ -2528,7 +2359,7 @@ def _reconsolidate(self, el: Element, default_key: str, parent_key: str): except BaseException as e: logger.exception("Effect cleanup %r raised exception %r", effect.callable, e) context.exceptions_self.append(e) - self._rerender_needed_reasons.append(RerenderReason(reason="Exception ocurred during effect")) + self._add_rerender_reason(RerenderReason(reason="Exception ocurred during effect")) self._rerender_needed = True context.needs_render = True effect = child_context.effects[effect_index] = effect.next @@ -2540,7 +2371,7 @@ def _reconsolidate(self, el: Element, default_key: str, parent_key: str): except BaseException as e: logger.exception("Effect %r raised exception %r", effect.callable, e) context.exceptions_self.append(e) - self._rerender_needed_reasons.append(RerenderReason(reason="Exception ocurred during effect")) + self._add_rerender_reason(RerenderReason(reason="Exception ocurred during effect")) self._rerender_needed = True context.needs_render = True else: @@ -2552,7 +2383,7 @@ def _reconsolidate(self, el: Element, default_key: str, parent_key: str): except BaseException as e: logger.exception("Effect %r raised exception %r", effect.callable, e) context.exceptions_self.append(e) - self._rerender_needed_reasons.append(RerenderReason(reason="Exception ocurred during effect")) + self._add_rerender_reason(RerenderReason(reason="Exception ocurred during effect")) self._rerender_needed = True context.needs_render = True @@ -2616,7 +2447,7 @@ def reconsolidate_children(): widget, orphan_ids = el._create_widget(kwargs) except BaseException as e: context.exceptions_self.append(e) - self._rerender_needed_reasons.append(RerenderReason(reason="Exception ocurred during reconciliation (creating widget)")) + self._add_rerender_reason(RerenderReason(reason="Exception ocurred during reconciliation (creating widget)")) self._rerender_needed = True if el.is_shared: self._shared_widgets[el] = widget @@ -2633,7 +2464,7 @@ def reconsolidate_children(): el._update_widget(widget_previous, el_prev, kwargs) except BaseException as e: context.exceptions_self.append(e) - self._rerender_needed_reasons.append(RerenderReason(reason="Exception ocurred during reconciliation (updating widget)")) + self._add_rerender_reason(RerenderReason(reason="Exception ocurred during reconciliation (updating widget)")) self._rerender_needed = True if el.is_shared: self._shared_widgets[el] = widget_previous @@ -2650,7 +2481,7 @@ def reconsolidate_children(): widget, orphan_ids = el._create_widget(kwargs) except BaseException as e: context.exceptions_self.append(e) - self._rerender_needed_reasons.append(RerenderReason(reason="Exception ocurred during reconciliation (updating widget)")) + self._add_rerender_reason(RerenderReason(reason="Exception ocurred during reconciliation (updating widget)")) self._rerender_needed = True if el.is_shared: self._shared_widgets[el] = widget @@ -2766,7 +2597,7 @@ def _remove_element(self, el: Element, default_key: str, parent_key): except BaseException as e: logger.exception("Effect cleanup %r raised exception %r", effect.callable, e) child_context.exceptions_self.append(e) - self._rerender_needed_reasons.append(RerenderReason(reason="Exception ocurred during effect")) + self._add_rerender_reason(RerenderReason(reason="Exception ocurred during effect")) self._rerender_needed = True for handler in child_context.event_handlers: try: @@ -2774,7 +2605,7 @@ def _remove_element(self, el: Element, default_key: str, parent_key): except BaseException as e: logger.exception("Removing event handler %r raised exception %r", handler, e) child_context.exceptions_self.append(e) - self._rerender_needed_reasons.append(RerenderReason(reason="Exception ocurred during effect")) + self._add_rerender_reason(RerenderReason(reason="Exception ocurred during effect")) self._rerender_needed = True assert self.context.root_element is not None new_parent_key = join_key(parent_key, key) @@ -2892,10 +2723,14 @@ class _RenderContextFast(_RenderContext): # # A new component (no previous context: a first render, a new list # item, another component type at a key) is mounted in one walk - # (_mount_component, _mount_node): it runs the bodies as the render phase - # does, creates the widgets of the new subtree children first, and writes - # the bookkeeping in its reconciled form. Reconciliation then only runs - # the effects, in the same order as before (_finish_mount). A mount is + # (_fastcore.mount_component): it runs the bodies as the render phase + # does, and creates the widgets of the new subtree children first. A + # mounted component (_MountedContext) keeps its element tree positionally + # (the widgets and child contexts in the order they were made); the dicts + # this class works with are made from that when they are first used + # (_fastcore.materialize), a mounted subtree that goes away is removed + # from it (_fastcore.remove_mounted). Reconciliation then only runs the + # effects, in the same order as before (_finish_mount). A mount is # undone into the render bookkeeping of the two phase walk (_unmount) # when the pass needs a next pass (state set during render), when a body # raised, for shared elements, and when a widget fails to be created. @@ -2907,26 +2742,23 @@ class _RenderContextFast(_RenderContext): # set to False to disable the fused mount (e.g. to compare) _mount_enabled = True - def __init__(self, *args, **kwargs): - # the mounted subtrees (their first component) that reconciliation still has to finish - self._mount_roots: Dict[ComponentContext, None] = {} - # the widgets created by mounts in this render pass: (context, key, element, widget) - self._mount_created: List[Tuple[ComponentContext, str, Element, widgets.Widget]] = [] - # the mounted components of the subtree that is being mounted, children first - self._mount_list: Optional[List[ComponentContext]] = None - # every component context made by mounts in this render pass - self._mount_contexts: List[ComponentContext] = [] - # the widgets constructed during the mount of a subtree (one recording per mount) - self._mount_recording: List[widgets.Widget] = [] - # something in this pass cannot be mounted: undo the mounts at the end of the pass - self._mount_failed = False - # records the elements a component body makes (see _call_component): one for all bodies, - # bodies of one render context do not nest - self._body_adder: ContainerAdder = ContainerAdder(None, "children") # type: ignore[arg-type] - super().__init__(*args, **kwargs) + # the mounted subtrees (their first component) that reconciliation still has to finish + _mount_roots: Dict[ComponentContext, None] + # the first components of the subtrees mounted in this render pass (see _unmount) + _mount_tops: List[ComponentContext] + # something in this pass cannot be mounted: undo the mounts at the end of the pass + _mount_failed: bool + # records the elements a component body makes (see _call_component): one for all bodies, + # bodies of one render context do not nest + _body_adder: ContainerAdder + + def __init__(self, element: Element, container: widgets.Widget = None, children_trait="children", handle_error: bool = True, initial_state=None): + _fastcore.init_render_context(self, element, container, children_trait, handle_error, True) + if initial_state: + self.state_set(self.context_root, initial_state) def _set_rerender_needed(self, reason: str): - self._rerender_needed_reasons.append(RerenderReason(reason=reason)) + self._add_rerender_reason(RerenderReason(reason=reason)) self._rerender_needed = True def _mark_dirty(self, context: ComponentContext): @@ -2948,47 +2780,21 @@ def _mark_dirty(self, context: ComponentContext): parent = parent.parent def _end_render_pass(self): - if self._mount_contexts: + if self._mount_tops: if self._rerender_needed or self._mount_failed or self.context_root.exceptions_children: # another pass (or no reconciliation at all) follows: go back to the plain two # phase state, as if the widgets were never created self._unmount() else: - self._mount_created = [] - self._mount_contexts = [] + self._mount_tops = [] self._mount_failed = False def _unmount(self): - widgets_dict = _get_widgets_dict() - for context, key, el, widget in self._mount_created: - for orphan in self._orphans.pop(widget.model_id, ()): - orphan_widget = widgets_dict.get(orphan) - if orphan_widget: - close_widget(orphan_widget) - el._cleanup_callbacks(widget) - el._close_widget(widget) - for context in self._mount_contexts: - # the render bookkeeping of the two phase walk - context.elements_next = context.elements - context.elements = {} - context.children_next = context.children - context.children = {} - context.root_element_next = context.root_element - context.root_element = None - context.widgets = {} - context.element_to_widget = {} - context.resolved_kwargs = {} - context.mounted = False - context.mount_widget = None - context.mount_order = None - self._mount_roots = {} - self._mount_created = [] - self._mount_contexts = [] - self._mount_list = None - self._mount_failed = False + # close the widgets of the mounts of this pass, back to the two phase render bookkeeping + _fastcore.undo_mounts(self) def _before_close(self): - if self._mount_contexts or self._mount_roots: + if self._mount_tops or self._mount_roots: # a reconciliation that raised did not finish them self._unmount() @@ -3011,33 +2817,11 @@ def _close_element(self, el: Element, default_key: str): child_context = context.children.get(key) if child_context is None: return - # the element is going away, pending exceptions only matter if cleanup fails - if child_context.exceptions_self: - child_context.exceptions_self = [] - if child_context.exceptions_children: - child_context.exceptions_children = [] - self.context = child_context - try: - for effect in child_context.effects: - if not effect._cleaned_up: - # Effect.cleanup, inline - cleanup = effect._cleanup - try: - if cleanup is not None: - cleanup() - except BaseException as e: - logger.exception("Effect cleanup %r raised exception %r", effect.callable, e) - child_context.exceptions_self.append(e) - effect._cleaned_up = True - root_element = child_context.root_element - if root_element is not None: - self._close_element(root_element, "/") - finally: - self.context = context - if child_context.exceptions_self or child_context.exceptions_children and not child_context.exception_handler: - # child does not handle exceptions, so bubble up - context.exceptions_children.extend(child_context.exceptions_self) - context.exceptions_children.extend(child_context.exceptions_children) + if child_context.nodes is not None: + # a mounted component: its tree is in its nodes + _fastcore.remove_mounted(self, child_context, True) + else: + self._close_component_context(child_context) else: if el._leaf is not True: self._walk_children_values(el.kwargs, key, "", self._close_child) @@ -3067,6 +2851,38 @@ def _close_element(self, el: Element, default_key: str): def _close_child(self, el: Element, key: str, parent_key: str): self._close_element(el, key) + def _close_component_context(self, child_context: ComponentContext): + # the close of a component (self.context is its parent): effect cleanups, then its tree + context = self.context + assert context is not None + # the element is going away, pending exceptions only matter if cleanup fails + if child_context.exceptions_self: + child_context.exceptions_self = [] + if child_context.exceptions_children: + child_context.exceptions_children = [] + self.context = child_context + try: + for effect in child_context.effects: + if not effect._cleaned_up: + # Effect.cleanup, inline + cleanup = effect._cleanup + try: + if cleanup is not None: + cleanup() + except BaseException as e: + logger.exception("Effect cleanup %r raised exception %r", effect.callable, e) + child_context.exceptions_self.append(e) + effect._cleaned_up = True + root_element = child_context.root_element + if root_element is not None: + self._close_element(root_element, "/") + finally: + self.context = context + if child_context.exceptions_self or child_context.exceptions_children and not child_context.exception_handler: + # child does not handle exceptions, so bubble up + context.exceptions_children.extend(child_context.exceptions_self) + context.exceptions_children.extend(child_context.exceptions_children) + def _discard_aborted_pass(self): self._unmount() super()._discard_aborted_pass() @@ -3083,7 +2899,7 @@ def _discard_aborted_pass(self): def _render(self, element: Element, default_key: str, parent_key: str): if not isinstance(element, Element): raise TypeError(f"Expected element, not {element}") - # (no _old_element_ids: get_widget recognizes a stale element by _key_frozen) + # (no _old_element_ids: get_widget recognizes a stale element by _render_count) context = self.context assert context is not None @@ -3098,7 +2914,6 @@ def _render(self, element: Element, default_key: str, parent_key: str): key = el._key if key is None: key = default_key - el._key_frozen = True if key in context.used_keys: if DEBUG: @@ -3119,7 +2934,7 @@ def _render(self, element: Element, default_key: str, parent_key: str): if el_prev is None: el_prev = context.elements.get(key) context.elements_next[key] = el - el._render_count += 1 # for testing only + el._render_count += 1 # (also freezes the key, see Element._key_frozen) if isinstance(el.component, ComponentWidget): assert not el.args, "no positional args supported for widgets" @@ -3199,14 +3014,14 @@ def _render_component(self, el: Element, key: str, parent_key: str, el_prev: Opt if self._mount_enabled and not el.is_shared and not DEBUG: # a new component without a widget yet: mount it (render + create widgets) in one walk if context_previous is None: - return self._mount_component(el, key, parent_context, parent_key, order) + return _fastcore.mount_component(self, el, key, parent_context, order, None) if context_previous.root_element is None and context_previous.root_element_next is None: # pre-created, carrying initial state (state_set), but never rendered - return self._mount_component(el, key, parent_context, parent_key, order, context_previous) + return _fastcore.mount_component(self, el, key, parent_context, order, context_previous) assert context_previous.invoke_element is not None if not same_component(context_previous.invoke_element.component, el.component): # a different component took this key (the old context is removed during reconciliation) - return self._mount_component(el, key, parent_context, parent_key, order) + return _fastcore.mount_component(self, el, key, parent_context, order, None) if context_previous is None: context = ComponentContext(parent=parent_context, context_managers=[cm(el) for cm in _component_context_manager_classes]) el_prev = None # do not compare against an element of a different component @@ -3378,276 +3193,17 @@ def _render_component(self, el: Element, key: str, parent_key: str, el_prev: Opt self._set_rerender_needed("Exception ocurred during render") parent_context.needs_render = True - def _mount_component( - self, el: Element, key: str, parent_context: ComponentContext, parent_key: str, order: int, context: Optional[ComponentContext] = None - ) -> Optional[widgets.Widget]: - # Mount a new component (no previous state; context is a context pre-created by - # state_set, or None): run the body with its hooks, mount the element tree it returns - # (_mount_node), and write the bookkeeping in its reconciled form. Returns the root - # widget, or None when this pass does not create widgets (any more). - mount_list = self._mount_list - first = mount_list is None - if first: - # one recording of the widgets constructed during the whole mount (to find the - # widgets made as a side effect of making a widget, like Layout and Style) - mount_list = self._mount_list = [] - recording: List[widgets.Widget] = [] - previous_recording = _start_recording_constructed(recording) - self._mount_recording = recording - try: - if context is None: - context = ComponentContext(parent=parent_context) - else: - context.parent = parent_context - if _component_context_manager_classes: - context.context_managers = [cm(el) for cm in _component_context_manager_classes] - context.invoke_element = el - context.key_in_parent = key - context.order_in_parent = order - self._mount_contexts.append(context) - if first: - # the parent renders in two phases - parent_context.children_next[key] = context - else: - parent_context.children[key] = context - - self.container_adders = [] - self.context = context - render_count_check = self.render_count - context.user_contexts = {} - context.needs_render = False - root_element: Optional[Element] = None - try: - context_managers = context.context_managers - if not context_managers: - root_element = self._call_component(el) - assert root_element is not None - elif len(context_managers) == 1: - with context_managers[0]: - root_element = self._call_component(el) - assert root_element is not None - else: - with contextlib.ExitStack() as stack: - for cm in context_managers: - stack.enter_context(cm) - root_element = self._call_component(el) - assert root_element is not None - except BaseException as e: - logger.exception("Component %r raised exception %r", el.component, e) - context.exceptions_self.append(e) - self._set_rerender_needed("Exception ocurred during render") - context.needs_render = True - if self.render_count != render_count_check: - raise RuntimeError("Recursive render detected, possible a bug in react") - context.needs_render_descendant = False - widget = None - if root_element is not None: - if el._event_handlers: - _add_event_handlers(root_element, el._event_handlers, context, self) - widget = self._mount_node(root_element, "/", join_key(parent_key, key)) - context.root_element = root_element - elif el.is_shared: - self._shared_elements_next.discard(el) - # (no hook count check: this render made the hooks) - if context.children_next: - # pre-created (state_set) children that were not used - context.children_next = {} - context.user_contexts_prev = context.user_contexts - self.context = parent_context - # exceptions: as in _render_component - if context.exceptions_self or context.exceptions_children and not context.exception_handler: - parent_context.exceptions_children.extend(context.exceptions_self) - parent_context.exceptions_children.extend(context.exceptions_children) - if context.exceptions_self or context.exceptions_children: - self._mark_dirty(context) - if parent_context.exceptions_self or parent_context.exceptions_children: - if not self._rerender_needed: - self._set_rerender_needed("Exception ocurred during render") - parent_context.needs_render = True - if widget is not None: - context.mount_widget = widget - context.mounted = True - assert mount_list is not None - mount_list.append(context) - return widget - finally: - if first: - _stop_recording_constructed(previous_recording) - self._mount_recording = [] - self._mount_list = None - if context is not None and context.mounted: - context.mount_order = mount_list - self._mount_roots[context] = None - elif mount_list: - # parts of the subtree were mounted, not all: undo at the end of the pass - self._mount_failed = True - - def _mount_node(self, element: Element, default_key: str, parent_key: str) -> Optional[widgets.Widget]: - # The mount walk of an element of the component self.context: returns its widget. - if not isinstance(element, Element): - raise TypeError(f"Expected element, not {element}") - el = element - context = self.context - assert context is not None - if default_key == "/": - context.used_keys.clear() - context.child_order_counter = 0 - key = el._key - if key is None: - key = default_key - el._key_frozen = True - used_keys = context.used_keys - if key in used_keys: - raise KeyError(f"Duplicate key {key!r}") - used_keys.add(key) - if el.is_shared: - # rendered once for the whole tree, by the two phase walk: undo this pass's mounts - self._mount_failed = True - context.has_shared = True - if el in self._shared_elements_next: - return None - self._shared_elements_next.add(el) - context.elements[key] = el - el._render_count += 1 # for testing only - - component = el.component - if isinstance(component, ComponentWidget): - assert not el.args, "no positional args supported for widgets" - elements_seen = self._elements_seen - kwargs = {} - on_kwargs = False - for name, value in el.kwargs.items(): - if name.startswith("on_"): - on_kwargs = True - if type(value) in _SCALAR_TYPES: - kwargs[name] = value - else: - kwargs[name] = self._visit_children_values(value, f"{key}{name}/", parent_key, self._mount_node) - has_elements = self._elements_seen != elements_seen - el._leaf = not has_elements - if self._rerender_needed or self._mount_failed: - # this pass will be undone, do not create more widgets - return None - if type(el)._create_widget is _element_create_widget: - # Element._create_widget, with the recording of this mount - listeners = None - if on_kwargs: - kwargs, listeners = el._split_kwargs(kwargs) - el._on_kwargs = on_kwargs - recording = self._mount_recording - count = len(recording) - try: - widget = component.widget(**kwargs) - except Exception: - # let reconciliation create it (and handle the exception) as it always does - self._mount_failed = True - return None - cls = type(widget) - if not getattr(cls.hold_trait_notifications, "_reacton_batched", False): - _install_batched_hold(cls) - widget._reacton_rc = self - if el._meta: - widget._react_meta = dict(el._meta) - if listeners: - for name, callback in listeners.items(): - if callback is not None: - el._add_widget_event_listener(widget, name, callback) - for handler in el._event_handlers: - handler._reacton_attach(widget) - orphan_ids = None - if len(recording) > count + 1 or (len(recording) == count + 1 and recording[count] is not widget): - widgets_dict = _get_widgets_dict() - orphan_ids = {w.model_id for w in recording[count:] if w is not widget and w.comm is not None and w.model_id in widgets_dict} - else: - try: - widget, orphan_ids = el._create_widget(kwargs) - except BaseException: - self._mount_failed = True - return None - self._mount_created.append((context, key, el, widget)) - context.widgets[key] = widget - context.element_to_widget[el] = widget - if has_elements: - context.resolved_kwargs[key] = kwargs - if orphan_ids: - for orphan_widget in [_get_widgets_dict()[k] for k in orphan_ids]: - if _is_shared_ipyvue_template(orphan_widget): - orphan_ids.discard(orphan_widget.model_id) - if orphan_ids: - self._orphans.setdefault(widget.model_id, set()).update(orphan_ids) - return widget - - # a component element - if el.is_shared and (el.args or el.kwargs): - self._visit_children_values(el.kwargs, key, parent_key, self._mount_node) - self._visit_children_values(el.args, key, parent_key, self._mount_node) - order = context.child_order_counter - context.child_order_counter = order + 1 - children_next = context.children_next - precreated = children_next.pop(key, None) if children_next else None - widget = self._mount_component(el, key, context, parent_key, order, precreated) - if widget is not None: - context.widgets[key] = widget - context.element_to_widget[el] = widget - if el._meta or getattr(widget, "_react_meta", {}): - widget._react_meta = {**getattr(widget, "_react_meta", {}), **el._meta} - return widget - def _finish_mount(self, root: ComponentContext): - # Reconciliation of a mounted subtree (self.context is the parent of root): the widgets - # and the bookkeeping are done, run the effects, children first, as the reconciliation - # walk would, and hook the root widget into the parent. - parent_context = self.context - assert parent_context is not None - mount_order = root.mount_order - assert mount_order is not None - try: - for context in mount_order: - parent = context.parent - assert parent is not None - context.mounted = False - effects = context.effects - if effects: - self.context = context - if context.exceptions_self or context.exceptions_children: - self._process_effects(context, parent) - else: - # _process_effects for effects that all run for the first time - for effect in effects: - if effect.next is not None or effect.executed: - self._process_effects(context, parent) - break - try: - effect._cleanup = effect.callable() - effect.executed = True - except BaseException as e: - logger.exception("Effect %r raised exception %r", effect.callable, e) - parent.exceptions_self.append(e) - self._set_rerender_needed("Exception ocurred during effect") - self._mark_dirty(parent) - parent.needs_render = True - if context.exceptions_self or context.exceptions_children and not context.exception_handler: - parent.exceptions_children.extend(context.exceptions_self) - parent.exceptions_children.extend(context.exceptions_children) - widget = root.mount_widget - el = root.invoke_element - key = root.key_in_parent - assert el is not None and widget is not None and key is not None - if el._meta or getattr(widget, "_react_meta", {}): - widget._react_meta = {**getattr(widget, "_react_meta", {}), **el._meta} - parent_context.widgets[key] = widget - parent_context.element_to_widget[el] = widget - finally: - self.context = parent_context - for context in mount_order: - context.mount_widget = None - root.mount_order = None - self._mount_roots.pop(root, None) + # Reconciliation of a mounted subtree (self.context is the parent of root): run the + # effects, children first, and hook the root widget into the parent + _fastcore.finish_mount(self, root) def _render_dirty_children(self, context: ComponentContext, dirty_children: Dict[ComponentContext, None], parent_key: str): # the render phase of a component that does not render again (self.context is its # context): render only the child components that are dirty, as the walk over its # (unchanged) element tree would, in the same order + # (a mounted component makes its dicts here: they give the keys and the order) + context.children if len(dirty_children) > 1: children = sorted(dirty_children, key=lambda child: child.order_in_parent) else: @@ -3661,40 +3217,12 @@ def _render_dirty_children(self, context: ComponentContext, dirty_children: Dict assert el is not None # reconciliation takes it from there, like after a walk of the element tree context.elements_next[key] = el - el._render_count += 1 # for testing only + el._render_count += 1 # (also freezes the key, see Element._key_frozen) self._render_component(el, key, parent_key, el, child.order_in_parent) def _call_component(self, el: Element) -> Optional[Element]: """Run the component function, with an implicit container when it returns None.""" - component: ComponentFunction = el.component # type: ignore[assignment] - default_container = _default_container - if default_container is None: - component.render_count += 1 - return component.f(*el.args, **el.kwargs) - # Only a body that returns None needs the implicit container. Building it for every - # body (an extra element, and collecting the top level elements from all elements the - # body made) costs more than a typical component body, so first only record the - # elements the body makes, like the container would. - adder = self._body_adder - created: List[Element] = [] - adder.created = created - container_adders = self.container_adders - container_adders.append(adder) - try: - component.render_count += 1 - root_element = component.f(*el.args, **el.kwargs) - finally: - container_adders.pop() - adder.created = _NO_ELEMENTS - if root_element is None: - with default_container() as container: - # the container collects the same elements, the same way - self.container_adders[-1].created.extend(created) - if len(container.kwargs["children"]) == 1: - root_element = container.kwargs["children"][0] - else: - root_element = container - return root_element + return _fastcore.call_component(self.container_adders, self._body_adder, el) def _reconsolidate(self, el: Element, default_key: str, parent_key: str): key = el._key @@ -4115,45 +3643,11 @@ def _remove_element(self, el: Element, default_key: str, parent_key): if el.is_shared: self._visit_children(el, key, parent_key, self._remove_element) child_context = context.children[key] - try: - self.context = child_context - # the element is going away, pending exceptions only matter if cleanup fails - child_context.exceptions_self = [] - child_context.exceptions_children = [] - for effect in child_context.effects: - try: - if not effect._cleaned_up: - effect.cleanup() - except BaseException as e: - logger.exception("Effect cleanup %r raised exception %r", effect.callable, e) - child_context.exceptions_self.append(e) - self._set_rerender_needed("Exception ocurred during effect") - self._mark_dirty(child_context) - for handler in child_context.event_handlers: - try: - handler._reacton_detach() - except BaseException as e: - logger.exception("Removing event handler %r raised exception %r", handler, e) - child_context.exceptions_self.append(e) - self._set_rerender_needed("Exception ocurred during effect") - self._mark_dirty(child_context) - assert child_context.root_element is not None - self._remove_element(child_context.root_element, "/", parent_key=join_key(parent_key, key)) - finally: - try: - assert not child_context.elements, f"left over elements {child_context.elements}" - assert not child_context.element_to_widget, f"left over element_to_widget {child_context.element_to_widget}" - assert not child_context.widgets, f"left over widgets {child_context.widgets}" - assert not child_context.children, f"left over children {child_context.children}" - assert not child_context.owns, f"left over owns {child_context.owns}" - except Exception as e: - child_context.exceptions_self.append(e) - # restore context - self.context = context - if child_context.exceptions_self or child_context.exceptions_children and not child_context.exception_handler: - # child does not handle exceptions, so bubble up - context.exceptions_children.extend(child_context.exceptions_self) - context.exceptions_children.extend(child_context.exceptions_children) + if child_context.nodes is not None: + # a mounted component: its tree is in its nodes + _fastcore.remove_mounted(self, child_context, False) + else: + self._remove_component_context(child_context) del context.children[key] else: self._visit_children(el, key, parent_key, self._remove_element) @@ -4180,6 +3674,51 @@ def _remove_element(self, el: Element, default_key: str, parent_key): del context.element_to_widget[el] del context.elements[key] + def _remove_component_context(self, child_context: ComponentContext): + # the removal of a component (self.context is its parent): effect cleanups, use_event + # handlers, then its tree + context = self.context + assert context is not None + try: + self.context = child_context + # the element is going away, pending exceptions only matter if cleanup fails + child_context.exceptions_self = [] + child_context.exceptions_children = [] + for effect in child_context.effects: + try: + if not effect._cleaned_up: + effect.cleanup() + except BaseException as e: + logger.exception("Effect cleanup %r raised exception %r", effect.callable, e) + child_context.exceptions_self.append(e) + self._set_rerender_needed("Exception ocurred during effect") + self._mark_dirty(child_context) + for handler in child_context.event_handlers: + try: + handler._reacton_detach() + except BaseException as e: + logger.exception("Removing event handler %r raised exception %r", handler, e) + child_context.exceptions_self.append(e) + self._set_rerender_needed("Exception ocurred during effect") + self._mark_dirty(child_context) + assert child_context.root_element is not None + self._remove_element(child_context.root_element, "/", parent_key="") + finally: + try: + assert not child_context.elements, f"left over elements {child_context.elements}" + assert not child_context.element_to_widget, f"left over element_to_widget {child_context.element_to_widget}" + assert not child_context.widgets, f"left over widgets {child_context.widgets}" + assert not child_context.children, f"left over children {child_context.children}" + assert not child_context.owns, f"left over owns {child_context.owns}" + except Exception as e: + child_context.exceptions_self.append(e) + # restore context + self.context = context + if child_context.exceptions_self or child_context.exceptions_children and not child_context.exception_handler: + # child does not handle exceptions, so bubble up + context.exceptions_children.extend(child_context.exceptions_self) + context.exceptions_children.extend(child_context.exceptions_children) + # The visitors below are the hottest code of a mount: they dispatch on the exact type, # skip scalars without a call, and only build key strings for values that can hold # elements. Subclasses of list/tuple/dict take the generic path, like before. @@ -4235,7 +3774,6 @@ def _render_children(self, value: Any, key: str, parent_key: str): child_key = v._key if child_key is None: child_key = f"{key}{k}/" - v._key_frozen = True used_keys = context.used_keys if child_key in used_keys: if DEBUG: @@ -4247,7 +3785,7 @@ def _render_children(self, value: Any, key: str, parent_key: str): if el_prev is None: el_prev = context.elements.get(child_key) elements_next[child_key] = v - v._render_count += 1 # for testing only + v._render_count += 1 # (also freezes the key, see Element._key_frozen) order = context.child_order_counter context.child_order_counter = order + 1 child = context.children.get(child_key) @@ -4438,6 +3976,10 @@ def _visit_children_values_generic(self, value: Any, key: str, parent_key: str, def _remove_stale_root_elements(self, parent_key): # remove stale elements of the root context itself # (child contexts are swept during their reconciliation) + root = self.context_root + if len(root.elements) <= len(root.used_keys) and not root.has_shared: + # (all used keys are in elements: only then the sizes tell there are none) + return stale_keys = sorted(set(self.context_root.elements) - self.context_root.used_keys) for stale_key in stale_keys: if stale_key in self.context_root.elements: @@ -4447,14 +3989,11 @@ def _remove_stale_root_elements(self, parent_key): # values that cannot hold elements (the child visitors skip them) _SCALAR_TYPES = frozenset([str, int, float, bool, complex, bytes, type(None)]) _element_cleanup_callbacks = Element._cleanup_callbacks -_element_create_widget = Element._create_widget _element_close_widget = Element._close_widget def _render_context_class(): - import os - - return _RenderContextFast if os.environ.get("REACTON_FAST") == "1" else _RenderContext + return _RenderContextFast if _fastcore.fast_selected() else _RenderContext @overload @@ -4471,15 +4010,22 @@ def render( def render(element: Element[T], container: widgets.Widget = None, children_trait="children", handle_error: bool = True, initial_state=None): container = container or widgets.VBox() - _rc = _render_context_class()(element, container, children_trait=children_trait, handle_error=handle_error, initial_state=initial_state) - _rc.render(element, _rc.container) + cls = _render_context_class() + _rc = cls(element, container, children_trait=children_trait, handle_error=handle_error, initial_state=initial_state) + if cls is _RenderContextFast: + _fastcore.render_first(_rc, element, _rc.container) + else: + _rc.render(element, _rc.container) local.last_rc = weakref.ref(_rc) return container, _rc def render_fixed(element: Element[T], handle_error: bool = True) -> Tuple[T, _RenderContext]: - _rc = _render_context_class()(element, handle_error=handle_error) - widget = _rc.render(element) + """Render element, without a container widget: returns its widget and the render context.""" + # (the implementation used is _fastcore.render_fixed, see the end of this module) + cls = _render_context_class() + _rc = cls(element, handle_error=handle_error) + widget = _fastcore.render_first(_rc, element, None) if cls is _RenderContextFast else _rc.render(element) local.last_rc = weakref.ref(_rc) return widget, _rc @@ -4600,3 +4146,54 @@ def Fragment(children: List[Element]): # not a public api yet, used in solara for now only. # lifecycle of context objects are linked to the lifecycle of the component _component_context_manager_classes: List[Any] = [] + + +_fastcore._register(Element, ValueElement) + +if not typing.TYPE_CHECKING: + # The hooks: for the fast renderer the work is done in _fastcore (compiled when reacton was + # built with Cython); any other render context gets its own methods, as above. + for _python_hook, _fast_hook in [ + (use_state, _fastcore.use_state), + (use_effect, _fastcore.use_effect), + (use_memo, _fastcore.use_memo), + (use_ref, _fastcore.use_ref), + ]: + _fast_hook.__doc__ = _python_hook.__doc__ + # (the same, with the render context made and the first render done without Python frames) + _fastcore.render_fixed.__doc__ = render_fixed.__doc__ + render_fixed = _fastcore.render_fixed + _fastcore.use_context.__doc__ = use_context.__doc__ + use_context = _fastcore.use_context + use_state = _fastcore.use_state + use_effect = _fastcore.use_effect + use_ref = _fastcore.use_ref + if _fastcore.__file__.endswith(".py"): + use_memo = _fastcore.use_memo + else: + _use_memo = _fastcore.use_memo + + def use_memo(f, dependencies=None, debug_name=None): + # A Python frame named use_memo: a compiled function has none, and solara.tasks + # looks for it on the stack (task() called inside use_memo does not warn). + return _use_memo(f, dependencies, debug_name) + + use_memo.__doc__ = _use_memo.__doc__ + + +# what _fastcore reads as its own globals (assignments to reacton.core are forwarded) +_FORWARDED = ("DEBUG", "_default_container", "_component_context_manager_classes") +_fastcore._default_container = _default_container +_fastcore._component_context_manager_classes = _component_context_manager_classes + + +class _CoreModule(types.ModuleType): + # reacton.core.DEBUG = ... (and the others in _FORWARDED) also sets it in _fastcore + def __setattr__(self, name, value): + super().__setattr__(name, value) + if name in _FORWARDED: + setattr(_fastcore, name, value) + + +sys.modules[__name__].__class__ = _CoreModule +_fastcore._register_core(sys.modules[__name__]) diff --git a/reacton/core_test.py b/reacton/core_test.py index 7d7056a..642e542 100644 --- a/reacton/core_test.py +++ b/reacton/core_test.py @@ -3325,6 +3325,24 @@ def Test(): rc.close() +def test_key_frozen_follows_render_count(): + # the frozen key is derived from the render count (no flag written per element render) + button = w.Button(description="a") + assert not button._key_frozen + button.key("before-render") + + @react.component + def Test(): + return w.VBox(children=[button]) + + box, rc = react.render(Test(), handle_error=False) + assert button._render_count == 1 + assert button._key_frozen + with pytest.raises(RuntimeError, match="Element keys should not be mutated after rendering"): + button.key("after-render") + rc.close() + + def test_render_nested_pass_widget_to_component_flip(): # One render() call, two passes: the first pass puts a widget element at a # slot, a state write during that pass opens a nested pass, and the nested @@ -4542,10 +4560,11 @@ def App(): _get_widgets_dict()[model_id].close() -def _close_log(fast: bool, fail_in_cleanup: bool = False): +def _close_log(fast: bool, fail_in_cleanup: bool = False, remove: bool = False): # close a tree with nested components, effects and listeners; log the order of the - # effect cleanups and the widget closes + # effect cleanups and the widget closes. remove: first remove the tree in an update log: List[str] = [] + set_show = lambda value: None # noqa class LoggedButton(widgets.Button): def close(self): @@ -4565,8 +4584,13 @@ def Box(name, children): @react.component def Leaf(name): value, set_value = react.use_state(0) + button = LoggedButton.element(description=name, on_click=lambda: set_value(value + 1)) def effect(): + if name.endswith("1"): + # (the fast renderer makes the dicts of this mounted component here) + react.get_widget(button) + def cleanup(): log.append(f"cleanup {name}") if fail_in_cleanup and name == "b1": @@ -4575,7 +4599,7 @@ def cleanup(): return cleanup react.use_effect(effect, []) - return LoggedButton.element(description=name, on_click=lambda: set_value(value + 1)) + return button @react.component def Group(name, n): @@ -4587,16 +4611,27 @@ def effect(): @react.component def App(): + nonlocal set_show + show, set_show = react.use_state(True) + def effect(): return lambda: log.append("cleanup app") react.use_effect(effect, []) + if not show: + return Box("app", [LoggedButton.element(description="first")]) return Box("app", [LoggedButton.element(description="first"), Group("a", 2), Box("inner", [Group("b", 2)]), Leaf("last")]) with unittest.mock.patch.dict(os.environ, {"REACTON_FAST": "1" if fast else "0"}): box, rc = react.render_fixed(App(), handle_error=False) assert isinstance(rc, core._RenderContextFast) == fast error = None + if remove: + try: + set_show(False) + except ValueError as e: + error = str(e) + log.append("removed") try: rc.close() except ValueError as e: @@ -4604,16 +4639,21 @@ def effect(): return log, error +@pytest.mark.parametrize("remove", [False, True]) @pytest.mark.parametrize("fail_in_cleanup", [False, True]) -def test_close_order_same_in_both_renderers(fail_in_cleanup): +def test_close_order_same_in_both_renderers(fail_in_cleanup, remove): + # (the fast renderer removes and closes a mounted subtree from its nodes; some of its + # components made their dicts, see Leaf) level = core.logger.level core.logger.setLevel(core.logging.CRITICAL) # a failing cleanup logs a traceback try: - default_log, default_error = _close_log(False, fail_in_cleanup) - fast_log, fast_error = _close_log(True, fail_in_cleanup) + default_log, default_error = _close_log(False, fail_in_cleanup, remove) + fast_log, fast_error = _close_log(True, fail_in_cleanup, remove) finally: core.logger.setLevel(level) assert "cleanup app" in default_log and "close first" in default_log + if remove: + assert default_log.index("close b1") < default_log.index("removed") assert fast_log == default_log assert fast_error == default_error if fail_in_cleanup: @@ -4673,6 +4713,27 @@ def Test(): assert class_ref() is None +def test_component_widget_per_class(): + # one ComponentWidget per widget class, kept on the class: a subclass gets its own + class Base(widgets.Button): + pass + + class Sub(Base): + pass + + base = react.core.ComponentWidget(widget=Base) + assert react.core.ComponentWidget(widget=Base) is base + sub = react.core.ComponentWidget(widget=Sub) + assert sub is not base + assert sub.widget is Sub + assert react.core.ComponentWidget(widget=Sub) is sub + assert react.core.ComponentWidget(widget=Base) is base + # a custom mime bundle is not shared + custom = react.core.ComponentWidget(widget=Base, mime_bundle={"text/plain": "custom"}) + assert custom is not base + assert react.core.ComponentWidget(widget=Base) is base + + def test_setter_is_stable(): # like React's setState: the same setter every render, so a child that gets it as an # argument sees equal arguments and does not render again @@ -4754,3 +4815,205 @@ def effect(): set_value(3) assert runs == ["a"] rc.close() + + +def _context_keys(rc): + # the keys of the dicts of every component context (the fast renderer makes the dicts of a + # mounted component from its nodes here) + out = {} + todo = [("", rc.context_root)] + while todo: + path, context = todo.pop() + out[path] = (sorted(context.elements), sorted(context.children), sorted(context.widgets), sorted(context.used_keys)) + todo.extend((f"{path}{key}|", child) for key, child in context.children.items()) + return out + + +def test_mounted_component_dicts_have_the_same_keys(): + # a mounted component keeps its tree positionally; the dicts made from it must have the + # keys of the two phase walk + @react.component + def Leaf(i): + return w.Button(description=f"leaf {i}") + + @react.component + def Wrapper(i): + return Leaf(i) + + @react.component + def App(): + return w.VBox( + children=[ + w.Label(value="first"), + Wrapper(1), + w.HBox(children=[Leaf(2), Leaf(3).key("three"), w.Button(description="x")]), + react.Fragment(children=[Leaf(4), w.Label(value="in fragment")]), + Leaf(5).key("five"), + ], + layout=w.Layout(width="10px"), + ) + + keys = {} + for fast in [False, True]: + with unittest.mock.patch.dict(os.environ, {"REACTON_FAST": "1" if fast else "0"}): + box, rc = react.render_fixed(App(), handle_error=False) + keys[fast] = _context_keys(rc) + rc.close() + assert keys[True] == keys[False] + assert "three" in keys[True]["/|"][1] + + +def test_state_get_restores_nested_state(): + set_values: Dict[str, Callable] = {} + + @react.component + def Child(name): + value, set_value = react.use_state(0) + set_values[name] = set_value + return w.Button(description=f"{name} {value}") + + @react.component + def Parent(): + value, set_value = react.use_state(0) + set_values["parent"] = set_value + return w.VBox(children=[w.Label(value=f"parent {value}"), Child("a"), Child("b").key("b")]) + + @react.component + def App(): + return w.HBox(children=[Parent()]) + + def descriptions(widget): + if isinstance(widget, widgets.Box): + return [descriptions(child) for child in widget.children] + return getattr(widget, "description", None) or widget.value + + states = {} + for fast in [False, True]: + with unittest.mock.patch.dict(os.environ, {"REACTON_FAST": "1" if fast else "0"}): + box, rc = react.render_fixed(App(), handle_error=False) + set_values["parent"](1) + set_values["a"](2) + set_values["b"](3) + state = states[fast] = rc.state_get() + rc.close() + # restored: the (mounted) components get their state back, at every level + container = widgets.VBox() + hbox, rc = react.render(App(), container, initial_state=state, handle_error=False) + assert descriptions(container.children[0]) == [["parent 1", "a 2", "b 3"]] + rc.close() + assert states[True] == states[False] + + +def test_first_render_when_an_effect_or_body_sets_state(): + # the first render of the fast renderer (_fastcore.render_first) hands over to the loop of + # render() when an effect sets state, or a body sets state or raises during the mount + events: List[str] = [] + + @react.component + def EffectSets(): + value, set_value = react.use_state(0) + react.use_effect(lambda: set_value(1), []) + events.append(f"effect-sets {value}") + return w.Button(description=f"effect {value}") + + @react.component + def BodySets(): + value, set_value = react.use_state(0) + if value == 0: + set_value(2) + events.append(f"body-sets {value}") + return w.Button(description=f"body {value}") + + @react.component + def Raises(): + raise ValueError("oops") + + @react.component + def Catches(): + exception, clear = react.use_exception() + return w.Label(value=f"caught {exception!r}") if exception else Raises() + + for fast in [False, True]: + with unittest.mock.patch.dict(os.environ, {"REACTON_FAST": "1" if fast else "0"}): + del events[:] + button, rc = react.render_fixed(EffectSets(), handle_error=False) + assert button.description == "effect 1" + assert events == ["effect-sets 0", "effect-sets 1"] + rc.close() + del events[:] + container = widgets.VBox() + box, rc = react.render(BodySets(), container, handle_error=False) + assert container.children[0].description == "body 2" + assert events == ["body-sets 0", "body-sets 2"] + rc.close() + label, rc = react.render_fixed(Catches(), handle_error=False) + assert label.value == "caught ValueError('oops')" + rc.close() + with pytest.raises(ValueError, match="oops"): + react.render_fixed(Raises(), handle_error=False) + + +def test_exception_in_cleanup_of_a_removed_child_reaches_the_handler(): + # an effect cleanup that raises when its component is removed: a component above it that + # handles exceptions (use_exception) gets it, in both renderers (the fast renderer removes + # a mounted child from its nodes) + set_show: Dict[str, Callable] = {} + + @react.component + def Child(): + def effect(): + def cleanup(): + raise ValueError("cleanup failed") + + return cleanup + + react.use_effect(effect, []) + return w.Button(description="child") + + @react.component + def Inner(): + show, set_show["inner"] = react.use_state(True) + return w.VBox(children=[Child(), Child()] if show else []) + + @react.component + def Catcher(): + exception, clear = react.use_exception() + return w.Label(value=f"caught {exception!r}") if exception else w.HBox(children=[Inner()]) + + results = {} + for fast in [False, True]: + with unittest.mock.patch.dict(os.environ, {"REACTON_FAST": "1" if fast else "0"}): + container = widgets.VBox() + box, rc = react.render(Catcher(), container, handle_error=False) + set_show["inner"](False) + results[fast] = container.children[0].value + rc.close() + assert results[True] == results[False] == "caught ValueError('cleanup failed')" + + +def test_exception_in_an_effect_of_a_new_subtree_bubbles_up(): + # an effect of a component deep in a new (mounted) subtree raises: the exception goes up + # through the components that do not handle it, in both renderers + @react.component + def Failing(): + def effect(): + raise ValueError("effect failed") + + react.use_effect(effect, []) + return w.Button(description="failing") + + @react.component + def Middle(): + return w.VBox(children=[w.Label(value="middle"), Failing()]) + + @react.component + def Catcher(): + exception, clear = react.use_exception() + return w.Label(value=f"caught {exception!r}") if exception else w.HBox(children=[Middle()]) + + for fast in [False, True]: + with unittest.mock.patch.dict(os.environ, {"REACTON_FAST": "1" if fast else "0"}): + container = widgets.VBox() + box, rc = react.render(Catcher(), container, handle_error=False) + assert container.children[0].value == "caught ValueError('effect failed')" + rc.close() diff --git a/reacton/fastcore_test.py b/reacton/fastcore_test.py new file mode 100644 index 0000000..28d15fc --- /dev/null +++ b/reacton/fastcore_test.py @@ -0,0 +1,158 @@ +"""The building blocks in reacton._fastcore, compiled (setup_cython.py) or plain Python.""" + +import copy +import pickle +import sys +import weakref +from types import FrameType +from typing import Optional + +import ipywidgets as widgets + +import reacton +import reacton.core +from reacton import ipywidgets as w +from reacton._fastcore_import import _fastcore + + +def test_element_is_a_normal_python_object(): + button = w.Button(description="hi") + assert isinstance(button, reacton.core.Element) + assert isinstance(button, _fastcore.ElementBase) + # arbitrary attributes, weak references, generic aliases (used in type hints) + button.custom = 1 + assert button.custom == 1 + assert weakref.ref(button)() is button + assert reacton.core.Element[widgets.Button] is not None + assert reacton.core.ValueElement[widgets.IntSlider, int] is not None + slider = w.IntSlider(value=1) + assert isinstance(slider, reacton.core.ValueElement) + assert slider.value_property == "value" + # defaults + element: reacton.core.Element = reacton.core.Element(reacton.core.ComponentWidget(widget=widgets.Button)) + assert element.args == [] and element.kwargs == {} + assert element._key is None and element._meta == {} and element._event_handlers == () + assert not element.is_shared and element._render_count == 0 and not element._key_frozen + assert element.shared() is element and element.is_shared and element._shared + + +def test_element_pickle_and_copy(): + element = w.Button(description="hi").key("my-key").meta(name="x") + element.custom = [1, 2] + for clone in [pickle.loads(pickle.dumps(element)), copy.copy(element)]: + assert type(clone) is type(element) + assert clone.kwargs == {"description": "hi"} + assert clone._key == "my-key" + assert clone._meta == {"name": "x"} + assert clone.custom == [1, 2] + assert clone.component is element.component + slider = pickle.loads(pickle.dumps(w.IntSlider(value=3))) + assert slider.value_property == "value" and slider.kwargs == {"value": 3} + + +def test_debug_keeps_the_frame_that_made_the_element(): + @reacton.component + def Child(): + return w.Button() + + assert _fastcore.DEBUG == reacton.core.DEBUG + reacton.core.DEBUG = True + try: + assert _fastcore.DEBUG + button = w.Button(description="made here") + child = Child() + finally: + reacton.core.DEBUG = False + assert not _fastcore.DEBUG + this_function = sys._getframe(0).f_code.co_name + assert button.traceback.tb_frame.f_code.co_name == this_function + assert child.traceback.tb_frame.f_code.co_name == this_function + + +def test_solara_context_manager_and_default_container(): + # solara appends a context manager class to reacton.core._component_context_manager_classes + # after importing reacton (solara/toestand.py), and assigns reacton.core._default_container + # (solara/components/__init__.py). The compiled mount reads both as its own globals. + log = [] + + class Manager: + def __init__(self, el): + self.name = el.component.name + + def __enter__(self): + log.append(("enter", self.name)) + + def __exit__(self, *args): + log.append(("exit", self.name)) + + @reacton.component + def MyColumn(children=[]): + log.append(("column", len(children))) + return w.VBox(children=children) + + set_value = lambda value: None # noqa + + @reacton.component + def Child(name): + # returns None: the implicit container gets the elements it made + w.Button(description=f"{name} a") + w.Button(description=f"{name} b") + + @reacton.component + def App(): + nonlocal set_value + value, set_value = reacton.use_state(0) + return w.HBox(children=[Child(f"child{value}").key(f"child{value}")]) + + previous_container = reacton.core._default_container + reacton.core._component_context_manager_classes.append(Manager) + reacton.core._default_container = MyColumn + try: + assert _fastcore._default_container is MyColumn + assert Manager in _fastcore._component_context_manager_classes + hbox, rc = reacton.render_fixed(App(), handle_error=False) + column = hbox.children[0] + assert isinstance(column, widgets.VBox) + assert [button.description for button in column.children] == ["child0 a", "child0 b"] + assert ("enter", "App") in log and ("exit", "App") in log + assert ("enter", "Child") in log and ("exit", "Child") in log + assert ("column", 2) in log + # a later update mounts a new child: the same container and manager + log.clear() + set_value(1) + column = hbox.children[0] + assert [button.description for button in column.children] == ["child1 a", "child1 b"] + assert ("enter", "Child") in log and ("exit", "Child") in log + assert ("column", 2) in log + rc.close() + finally: + reacton.core._component_context_manager_classes.remove(Manager) + reacton.core._default_container = previous_container + assert _fastcore._default_container is previous_container + assert Manager not in _fastcore._component_context_manager_classes + + +def test_use_memo_has_a_frame(): + # solara.tasks: task() called inside use_memo does not warn. It checks the 5 frames above + # the user code that calls task() for a function named use_memo in a reacton module. + found = [] + + def make(): + frame: Optional[FrameType] = sys._getframe(2) # the frame above the lambda + for _ in range(5): + if frame is None: + break + if frame.f_code.co_name == "use_memo" and frame.f_globals.get("__name__", "").startswith("reacton."): + found.append(frame.f_globals["__name__"]) + break + frame = frame.f_back + return 1 + + @reacton.component + def Test(): + reacton.use_memo(lambda: make(), []) + return w.Button() + + box, rc = reacton.render(Test(), handle_error=False) + rc.close() + assert len(found) == 1 diff --git a/reacton/generate.py b/reacton/generate.py index 9fc7ea9..3decde6 100644 --- a/reacton/generate.py +++ b/reacton/generate.py @@ -258,14 +258,14 @@ def get_type(trait): if "v_model" in traits: element_type = f'ValueElement[{class_name}, {types["v_model"]}]' element_class_name = "ValueElement" - create_element = f'{element_class_name}("v_model", comp, kwargs=kwargs)' + create_element = f'{element_class_name}("v_model", _{method_name}_component, kwargs=kwargs)' elif "value" in traits: element_type = f'ValueElement[{class_name}, {types["value"]}]' element_class_name = "ValueElement" - create_element = f'{element_class_name}("value", comp, kwargs=kwargs)' + create_element = f'{element_class_name}("value", _{method_name}_component, kwargs=kwargs)' else: element_type = f"Element[{class_name}]" - create_element = f"{element_class_name}(comp, kwargs=kwargs)" + create_element = f"{element_class_name}(_{method_name}_component, kwargs=kwargs)" docstring_args_template = Template( """ @@ -276,6 +276,8 @@ def get_type(trait): docstring_args = docstring_args_template.render(docargs=docargs) docstring_args = indent(docstring_args, " ").strip() + # The element factory uses one ComponentWidget per widget class, made when the module + # is imported (not one lookup per element). code_method = Template( """ @@ -288,12 +290,11 @@ def _{{ method_name }}({{ signature }}) -> {{element_type}}: @implements(_{{ method_name }}) def {{ method_name }}(**kwargs): {{InstanceDict_fixes}} - widget_cls = {{class_name}} - comp = reacton.core.ComponentWidget(widget=widget_cls) return {{create_element}} del _{{ method_name }} +_{{ method_name }}_component = reacton.core.ComponentWidget(widget={{class_name}}) """ ) diff --git a/reacton/generate_test.py b/reacton/generate_test.py index 64bb62c..5a751a8 100644 --- a/reacton/generate_test.py +++ b/reacton/generate_test.py @@ -25,12 +25,11 @@ def _MyTest( @implements(_MyTest) def MyTest(**kwargs): - widget_cls = reacton.generate_test.MyTest - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_MyTest_component, kwargs=kwargs) del _MyTest +_MyTest_component = reacton.core.ComponentWidget(widget=reacton.generate_test.MyTest) ''' assert code.strip() == code_expected.strip() @@ -54,12 +53,11 @@ def _MyTest( @implements(_MyTest) def MyTest(**kwargs): - widget_cls = reacton.generate_test.MyTest - comp = reacton.core.ComponentWidget(widget=widget_cls) - return ValueElement("value", comp, kwargs=kwargs) + return ValueElement("value", _MyTest_component, kwargs=kwargs) del _MyTest +_MyTest_component = reacton.core.ComponentWidget(widget=reacton.generate_test.MyTest) ''' assert code.strip() == code_expected.strip() @@ -88,12 +86,11 @@ def _MyTest( @implements(_MyTest) def MyTest(**kwargs): - widget_cls = reacton.generate_test.MyTest - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_MyTest_component, kwargs=kwargs) -del _MyTest''' +del _MyTest +_MyTest_component = reacton.core.ComponentWidget(widget=reacton.generate_test.MyTest)''' assert code.strip() == code_expected.strip() @@ -119,12 +116,11 @@ def _MyTest( @implements(_MyTest) def MyTest(**kwargs): - widget_cls = reacton.generate_test.MyTest - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_MyTest_component, kwargs=kwargs) del _MyTest +_MyTest_component = reacton.core.ComponentWidget(widget=reacton.generate_test.MyTest) ''' assert code.strip() == code_expected.strip() diff --git a/reacton/ipycanvas.py b/reacton/ipycanvas.py index b02b72a..b7a36b6 100644 --- a/reacton/ipycanvas.py +++ b/reacton/ipycanvas.py @@ -82,12 +82,11 @@ def _Canvas( def Canvas(**kwargs): if isinstance(kwargs.get("layout"), dict): kwargs["layout"] = w.Layout(**kwargs["layout"]) - widget_cls = ipycanvas.canvas.Canvas - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_Canvas_component, kwargs=kwargs) del _Canvas +_Canvas_component = reacton.core.ComponentWidget(widget=ipycanvas.canvas.Canvas) def _MultiCanvas( @@ -118,12 +117,11 @@ def _MultiCanvas( def MultiCanvas(**kwargs): if isinstance(kwargs.get("layout"), dict): kwargs["layout"] = w.Layout(**kwargs["layout"]) - widget_cls = ipycanvas.canvas.MultiCanvas - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_MultiCanvas_component, kwargs=kwargs) del _MultiCanvas +_MultiCanvas_component = reacton.core.ComponentWidget(widget=ipycanvas.canvas.MultiCanvas) def _MultiRoughCanvas( @@ -154,12 +152,11 @@ def _MultiRoughCanvas( def MultiRoughCanvas(**kwargs): if isinstance(kwargs.get("layout"), dict): kwargs["layout"] = w.Layout(**kwargs["layout"]) - widget_cls = ipycanvas.canvas.MultiRoughCanvas - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_MultiRoughCanvas_component, kwargs=kwargs) del _MultiRoughCanvas +_MultiRoughCanvas_component = reacton.core.ComponentWidget(widget=ipycanvas.canvas.MultiRoughCanvas) def _Path2D(value: str = "", on_value: typing.Callable[[str], Any] = None) -> Element[ipycanvas.canvas.Path2D]: @@ -175,12 +172,11 @@ def _Path2D(value: str = "", on_value: typing.Callable[[str], Any] = None) -> El @implements(_Path2D) def Path2D(**kwargs): - widget_cls = ipycanvas.canvas.Path2D - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_Path2D_component, kwargs=kwargs) del _Path2D +_Path2D_component = reacton.core.ComponentWidget(widget=ipycanvas.canvas.Path2D) def _RoughCanvas( @@ -252,9 +248,8 @@ def _RoughCanvas( def RoughCanvas(**kwargs): if isinstance(kwargs.get("layout"), dict): kwargs["layout"] = w.Layout(**kwargs["layout"]) - widget_cls = ipycanvas.canvas.RoughCanvas - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_RoughCanvas_component, kwargs=kwargs) del _RoughCanvas +_RoughCanvas_component = reacton.core.ComponentWidget(widget=ipycanvas.canvas.RoughCanvas) diff --git a/reacton/ipyvue.py b/reacton/ipyvue.py index 188a044..dc7e496 100644 --- a/reacton/ipyvue.py +++ b/reacton/ipyvue.py @@ -1,103 +1,7 @@ -from typing import Any, Callable, Optional +from reacton.core import _fastcore -import ipyvue -import reacton as react -from reacton.core import ComponentWidget, _add_event_handlers, local - - -class _EventHandler: - """The handler of one use_event hook, made once (like a stable setter). - - The renderer registers it on the widget of the element when that widget is created or - updated (Element._event_handlers), and removes it when the component of the hook goes - away. It calls the latest callback given to use_event. - """ - - __slots__ = ("rc", "context", "callback", "event", "widget", "registered_event", "removed") - - def __init__(self, rc, context, event_and_modifiers: str, callback: Callable[[Any], Any]): - self.rc = rc - self.context = context - self.callback = callback - self.event = event_and_modifiers - self.widget: Optional[ipyvue.VueWidget] = None - self.registered_event: Optional[str] = None - # the hook is gone: never register again (an element can outlive the hook, e.g. a - # memoized element of a parent that gets a new widget later) - self.removed = False - - def __call__(self, *args): - try: - self.callback(*args) - except Exception as e: - # because widgets don't have a context, but are a child of a component - # we add it to exceptions_children, not exception_self - # this allows a component to catch the exception of a direct child - self.context.exceptions_children.append(e) - self.rc.force_update() - - def _reacton_attach(self, widget): - if self.removed: - return - event = self.event - previous = self.widget - if widget is previous and event == self.registered_event: - return - if previous is not None and previous.comm is not None and self.registered_event is not None: - previous.on_event(self.registered_event, self, remove=True) - widget.on_event(event, self) - self.widget = widget - self.registered_event = event - - def _reacton_detach(self): - self.removed = True - widget = self.widget - self.widget = None - if widget is None or self.rc._closing: - # the whole tree is going away: removing the handler would sync - # the _events trait to the frontend (one message per widget) - # right before the comm is closed anyway - return - if widget.comm is not None: - widget.on_event(self.registered_event, self, remove=True) - - -def use_event(el: react.core.Element, event_and_modifiers, callback: Callable[[Any], Any]): - rc = getattr(local, "rc", None) - if rc is None: - raise RuntimeError("No render context") - context = rc.context - assert context is not None - ref = rc.use_ref(None) - handler = ref.current - if handler is None: - handler = ref.current = _EventHandler(rc, context, event_and_modifiers, callback) - context.event_handlers = (*context.event_handlers, handler) - else: - handler.callback = callback - if event_and_modifiers != handler.event: - handler.event = event_and_modifiers - if handler.widget is not None: - handler._reacton_attach(handler.widget) - - # Put the event name in the widget constructor arguments: the synced _events - # trait then goes along with the comm open message. The later on_event call - # only updates _events when the event set differs, so this saves one update - # message per widget per event. When the element is reused from a previous - # render (memoized) and the widget already exists, on_event falls back to - # syncing _events itself. - if isinstance(el.component, ComponentWidget) and issubclass(el.component.widget, ipyvue.VueWidget): - events = el.kwargs.get("_events") - if events is None: - el.kwargs["_events"] = [event_and_modifiers] - elif event_and_modifiers not in events: - # do not mutate the list, it could be shared with a previous element - el.kwargs["_events"] = [*events, event_and_modifiers] - - handlers = el._event_handlers - if handler not in handlers: - if el._key_frozen: - _add_event_handlers(el, (handler,), context, rc) - else: - el._event_handlers = (*handlers, handler) +# The handler object and the hook are in _fastcore (compiled when reacton was built with +# Cython): use_event is called for every button of a page. +_EventHandler = _fastcore._EventHandler +use_event = _fastcore.use_event diff --git a/reacton/ipywidgets.py b/reacton/ipywidgets.py index 99ece34..fdb7e5a 100644 --- a/reacton/ipywidgets.py +++ b/reacton/ipywidgets.py @@ -159,12 +159,11 @@ def _Accordion( def Accordion(**kwargs): if isinstance(kwargs.get("layout"), dict): kwargs["layout"] = Layout(**kwargs["layout"]) - widget_cls = ipywidgets.widgets.widget_selectioncontainer.Accordion - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_Accordion_component, kwargs=kwargs) del _Accordion +_Accordion_component = reacton.core.ComponentWidget(widget=ipywidgets.widgets.widget_selectioncontainer.Accordion) def _AppLayout( @@ -257,12 +256,11 @@ def _AppLayout( def AppLayout(**kwargs): if isinstance(kwargs.get("layout"), dict): kwargs["layout"] = Layout(**kwargs["layout"]) - widget_cls = ipywidgets.widgets.widget_templates.AppLayout - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_AppLayout_component, kwargs=kwargs) del _AppLayout +_AppLayout_component = reacton.core.ComponentWidget(widget=ipywidgets.widgets.widget_templates.AppLayout) def _Audio( @@ -302,12 +300,11 @@ def _Audio( def Audio(**kwargs): if isinstance(kwargs.get("layout"), dict): kwargs["layout"] = Layout(**kwargs["layout"]) - widget_cls = ipywidgets.widgets.widget_media.Audio - comp = reacton.core.ComponentWidget(widget=widget_cls) - return ValueElement("value", comp, kwargs=kwargs) + return ValueElement("value", _Audio_component, kwargs=kwargs) del _Audio +_Audio_component = reacton.core.ComponentWidget(widget=ipywidgets.widgets.widget_media.Audio) def _BoundedFloatText( @@ -368,12 +365,11 @@ def BoundedFloatText(**kwargs): kwargs["layout"] = Layout(**kwargs["layout"]) if isinstance(kwargs.get("style"), dict): kwargs["style"] = DescriptionStyle(**kwargs["style"]) - widget_cls = ipywidgets.widgets.widget_float.BoundedFloatText - comp = reacton.core.ComponentWidget(widget=widget_cls) - return ValueElement("value", comp, kwargs=kwargs) + return ValueElement("value", _BoundedFloatText_component, kwargs=kwargs) del _BoundedFloatText +_BoundedFloatText_component = reacton.core.ComponentWidget(widget=ipywidgets.widgets.widget_float.BoundedFloatText) def _BoundedIntText( @@ -419,12 +415,11 @@ def BoundedIntText(**kwargs): kwargs["layout"] = Layout(**kwargs["layout"]) if isinstance(kwargs.get("style"), dict): kwargs["style"] = DescriptionStyle(**kwargs["style"]) - widget_cls = ipywidgets.widgets.widget_int.BoundedIntText - comp = reacton.core.ComponentWidget(widget=widget_cls) - return ValueElement("value", comp, kwargs=kwargs) + return ValueElement("value", _BoundedIntText_component, kwargs=kwargs) del _BoundedIntText +_BoundedIntText_component = reacton.core.ComponentWidget(widget=ipywidgets.widgets.widget_int.BoundedIntText) def _Box( @@ -466,12 +461,11 @@ def _Box( def Box(**kwargs): if isinstance(kwargs.get("layout"), dict): kwargs["layout"] = Layout(**kwargs["layout"]) - widget_cls = ipywidgets.widgets.widget_box.Box - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_Box_component, kwargs=kwargs) del _Box +_Box_component = reacton.core.ComponentWidget(widget=ipywidgets.widgets.widget_box.Box) def _Button( @@ -522,12 +516,11 @@ def Button(**kwargs): kwargs["layout"] = Layout(**kwargs["layout"]) if isinstance(kwargs.get("style"), dict): kwargs["style"] = ButtonStyle(**kwargs["style"]) - widget_cls = ipywidgets.widgets.widget_button.Button - comp = reacton.core.ComponentWidget(widget=widget_cls) - return ButtonElement(comp, kwargs=kwargs) + return ButtonElement(_Button_component, kwargs=kwargs) del _Button +_Button_component = reacton.core.ComponentWidget(widget=ipywidgets.widgets.widget_button.Button) def _ButtonStyle( @@ -545,12 +538,11 @@ def _ButtonStyle( @implements(_ButtonStyle) def ButtonStyle(**kwargs): - widget_cls = ipywidgets.widgets.widget_button.ButtonStyle - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_ButtonStyle_component, kwargs=kwargs) del _ButtonStyle +_ButtonStyle_component = reacton.core.ComponentWidget(widget=ipywidgets.widgets.widget_button.ButtonStyle) def _Checkbox( @@ -596,12 +588,11 @@ def Checkbox(**kwargs): kwargs["layout"] = Layout(**kwargs["layout"]) if isinstance(kwargs.get("style"), dict): kwargs["style"] = DescriptionStyle(**kwargs["style"]) - widget_cls = ipywidgets.widgets.widget_bool.Checkbox - comp = reacton.core.ComponentWidget(widget=widget_cls) - return ValueElement("value", comp, kwargs=kwargs) + return ValueElement("value", _Checkbox_component, kwargs=kwargs) del _Checkbox +_Checkbox_component = reacton.core.ComponentWidget(widget=ipywidgets.widgets.widget_bool.Checkbox) def _ColorPicker( @@ -637,12 +628,11 @@ def ColorPicker(**kwargs): kwargs["layout"] = Layout(**kwargs["layout"]) if isinstance(kwargs.get("style"), dict): kwargs["style"] = DescriptionStyle(**kwargs["style"]) - widget_cls = ipywidgets.widgets.widget_color.ColorPicker - comp = reacton.core.ComponentWidget(widget=widget_cls) - return ValueElement("value", comp, kwargs=kwargs) + return ValueElement("value", _ColorPicker_component, kwargs=kwargs) del _ColorPicker +_ColorPicker_component = reacton.core.ComponentWidget(widget=ipywidgets.widgets.widget_color.ColorPicker) def _Combobox( @@ -688,12 +678,11 @@ def Combobox(**kwargs): kwargs["layout"] = Layout(**kwargs["layout"]) if isinstance(kwargs.get("style"), dict): kwargs["style"] = DescriptionStyle(**kwargs["style"]) - widget_cls = ipywidgets.widgets.widget_string.Combobox - comp = reacton.core.ComponentWidget(widget=widget_cls) - return ValueElement("value", comp, kwargs=kwargs) + return ValueElement("value", _Combobox_component, kwargs=kwargs) del _Combobox +_Combobox_component = reacton.core.ComponentWidget(widget=ipywidgets.widgets.widget_string.Combobox) def _Controller( @@ -730,12 +719,11 @@ def _Controller( def Controller(**kwargs): if isinstance(kwargs.get("layout"), dict): kwargs["layout"] = Layout(**kwargs["layout"]) - widget_cls = ipywidgets.widgets.widget_controller.Controller - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_Controller_component, kwargs=kwargs) del _Controller +_Controller_component = reacton.core.ComponentWidget(widget=ipywidgets.widgets.widget_controller.Controller) def _CoreWidget() -> Element[ipywidgets.widgets.widget_core.CoreWidget]: @@ -745,12 +733,11 @@ def _CoreWidget() -> Element[ipywidgets.widgets.widget_core.CoreWidget]: @implements(_CoreWidget) def CoreWidget(**kwargs): - widget_cls = ipywidgets.widgets.widget_core.CoreWidget - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_CoreWidget_component, kwargs=kwargs) del _CoreWidget +_CoreWidget_component = reacton.core.ComponentWidget(widget=ipywidgets.widgets.widget_core.CoreWidget) def _DOMWidget( @@ -765,12 +752,11 @@ def _DOMWidget( def DOMWidget(**kwargs): if isinstance(kwargs.get("layout"), dict): kwargs["layout"] = Layout(**kwargs["layout"]) - widget_cls = ipywidgets.widgets.domwidget.DOMWidget - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_DOMWidget_component, kwargs=kwargs) del _DOMWidget +_DOMWidget_component = reacton.core.ComponentWidget(widget=ipywidgets.widgets.domwidget.DOMWidget) def _DatePicker( @@ -821,12 +807,11 @@ def DatePicker(**kwargs): kwargs["layout"] = Layout(**kwargs["layout"]) if isinstance(kwargs.get("style"), dict): kwargs["style"] = DescriptionStyle(**kwargs["style"]) - widget_cls = ipywidgets.widgets.widget_date.DatePicker - comp = reacton.core.ComponentWidget(widget=widget_cls) - return ValueElement("value", comp, kwargs=kwargs) + return ValueElement("value", _DatePicker_component, kwargs=kwargs) del _DatePicker +_DatePicker_component = reacton.core.ComponentWidget(widget=ipywidgets.widgets.widget_date.DatePicker) def _Dropdown( @@ -902,12 +887,11 @@ def Dropdown(**kwargs): kwargs["layout"] = Layout(**kwargs["layout"]) if isinstance(kwargs.get("style"), dict): kwargs["style"] = DescriptionStyle(**kwargs["style"]) - widget_cls = ipywidgets.widgets.widget_selection.Dropdown - comp = reacton.core.ComponentWidget(widget=widget_cls) - return ValueElement("value", comp, kwargs=kwargs) + return ValueElement("value", _Dropdown_component, kwargs=kwargs) del _Dropdown +_Dropdown_component = reacton.core.ComponentWidget(widget=ipywidgets.widgets.widget_selection.Dropdown) def _FileUpload( @@ -961,12 +945,11 @@ def FileUpload(**kwargs): kwargs["layout"] = Layout(**kwargs["layout"]) if isinstance(kwargs.get("style"), dict): kwargs["style"] = ButtonStyle(**kwargs["style"]) - widget_cls = ipywidgets.widgets.widget_upload.FileUpload - comp = reacton.core.ComponentWidget(widget=widget_cls) - return ValueElement("value", comp, kwargs=kwargs) + return ValueElement("value", _FileUpload_component, kwargs=kwargs) del _FileUpload +_FileUpload_component = reacton.core.ComponentWidget(widget=ipywidgets.widgets.widget_upload.FileUpload) def _FloatLogSlider( @@ -1046,12 +1029,11 @@ def FloatLogSlider(**kwargs): kwargs["layout"] = Layout(**kwargs["layout"]) if isinstance(kwargs.get("style"), dict): kwargs["style"] = SliderStyle(**kwargs["style"]) - widget_cls = ipywidgets.widgets.widget_float.FloatLogSlider - comp = reacton.core.ComponentWidget(widget=widget_cls) - return ValueElement("value", comp, kwargs=kwargs) + return ValueElement("value", _FloatLogSlider_component, kwargs=kwargs) del _FloatLogSlider +_FloatLogSlider_component = reacton.core.ComponentWidget(widget=ipywidgets.widgets.widget_float.FloatLogSlider) def _FloatProgress( @@ -1109,12 +1091,11 @@ def FloatProgress(**kwargs): kwargs["layout"] = Layout(**kwargs["layout"]) if isinstance(kwargs.get("style"), dict): kwargs["style"] = ProgressStyle(**kwargs["style"]) - widget_cls = ipywidgets.widgets.widget_float.FloatProgress - comp = reacton.core.ComponentWidget(widget=widget_cls) - return ValueElement("value", comp, kwargs=kwargs) + return ValueElement("value", _FloatProgress_component, kwargs=kwargs) del _FloatProgress +_FloatProgress_component = reacton.core.ComponentWidget(widget=ipywidgets.widgets.widget_float.FloatProgress) def _FloatRangeSlider( @@ -1189,12 +1170,11 @@ def FloatRangeSlider(**kwargs): kwargs["layout"] = Layout(**kwargs["layout"]) if isinstance(kwargs.get("style"), dict): kwargs["style"] = SliderStyle(**kwargs["style"]) - widget_cls = ipywidgets.widgets.widget_float.FloatRangeSlider - comp = reacton.core.ComponentWidget(widget=widget_cls) - return ValueElement("value", comp, kwargs=kwargs) + return ValueElement("value", _FloatRangeSlider_component, kwargs=kwargs) del _FloatRangeSlider +_FloatRangeSlider_component = reacton.core.ComponentWidget(widget=ipywidgets.widgets.widget_float.FloatRangeSlider) def _FloatSlider( @@ -1269,12 +1249,11 @@ def FloatSlider(**kwargs): kwargs["layout"] = Layout(**kwargs["layout"]) if isinstance(kwargs.get("style"), dict): kwargs["style"] = SliderStyle(**kwargs["style"]) - widget_cls = ipywidgets.widgets.widget_float.FloatSlider - comp = reacton.core.ComponentWidget(widget=widget_cls) - return ValueElement("value", comp, kwargs=kwargs) + return ValueElement("value", _FloatSlider_component, kwargs=kwargs) del _FloatSlider +_FloatSlider_component = reacton.core.ComponentWidget(widget=ipywidgets.widgets.widget_float.FloatSlider) def _FloatText( @@ -1324,12 +1303,11 @@ def FloatText(**kwargs): kwargs["layout"] = Layout(**kwargs["layout"]) if isinstance(kwargs.get("style"), dict): kwargs["style"] = DescriptionStyle(**kwargs["style"]) - widget_cls = ipywidgets.widgets.widget_float.FloatText - comp = reacton.core.ComponentWidget(widget=widget_cls) - return ValueElement("value", comp, kwargs=kwargs) + return ValueElement("value", _FloatText_component, kwargs=kwargs) del _FloatText +_FloatText_component = reacton.core.ComponentWidget(widget=ipywidgets.widgets.widget_float.FloatText) def _GridBox( @@ -1367,12 +1345,11 @@ def _GridBox( def GridBox(**kwargs): if isinstance(kwargs.get("layout"), dict): kwargs["layout"] = Layout(**kwargs["layout"]) - widget_cls = ipywidgets.widgets.widget_box.GridBox - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_GridBox_component, kwargs=kwargs) del _GridBox +_GridBox_component = reacton.core.ComponentWidget(widget=ipywidgets.widgets.widget_box.GridBox) def _GridspecLayout( @@ -1447,12 +1424,11 @@ def _GridspecLayout( def GridspecLayout(**kwargs): if isinstance(kwargs.get("layout"), dict): kwargs["layout"] = Layout(**kwargs["layout"]) - widget_cls = ipywidgets.widgets.widget_templates.GridspecLayout - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_GridspecLayout_component, kwargs=kwargs) del _GridspecLayout +_GridspecLayout_component = reacton.core.ComponentWidget(widget=ipywidgets.widgets.widget_templates.GridspecLayout) def _HBox( @@ -1492,12 +1468,11 @@ def _HBox( def HBox(**kwargs): if isinstance(kwargs.get("layout"), dict): kwargs["layout"] = Layout(**kwargs["layout"]) - widget_cls = ipywidgets.widgets.widget_box.HBox - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_HBox_component, kwargs=kwargs) del _HBox +_HBox_component = reacton.core.ComponentWidget(widget=ipywidgets.widgets.widget_box.HBox) def _HTML( @@ -1530,12 +1505,11 @@ def HTML(**kwargs): kwargs["layout"] = Layout(**kwargs["layout"]) if isinstance(kwargs.get("style"), dict): kwargs["style"] = DescriptionStyle(**kwargs["style"]) - widget_cls = ipywidgets.widgets.widget_string.HTML - comp = reacton.core.ComponentWidget(widget=widget_cls) - return ValueElement("value", comp, kwargs=kwargs) + return ValueElement("value", _HTML_component, kwargs=kwargs) del _HTML +_HTML_component = reacton.core.ComponentWidget(widget=ipywidgets.widgets.widget_string.HTML) def _HTMLMath( @@ -1568,12 +1542,11 @@ def HTMLMath(**kwargs): kwargs["layout"] = Layout(**kwargs["layout"]) if isinstance(kwargs.get("style"), dict): kwargs["style"] = DescriptionStyle(**kwargs["style"]) - widget_cls = ipywidgets.widgets.widget_string.HTMLMath - comp = reacton.core.ComponentWidget(widget=widget_cls) - return ValueElement("value", comp, kwargs=kwargs) + return ValueElement("value", _HTMLMath_component, kwargs=kwargs) del _HTMLMath +_HTMLMath_component = reacton.core.ComponentWidget(widget=ipywidgets.widgets.widget_string.HTMLMath) def _Image( @@ -1610,12 +1583,11 @@ def _Image( def Image(**kwargs): if isinstance(kwargs.get("layout"), dict): kwargs["layout"] = Layout(**kwargs["layout"]) - widget_cls = ipywidgets.widgets.widget_media.Image - comp = reacton.core.ComponentWidget(widget=widget_cls) - return ValueElement("value", comp, kwargs=kwargs) + return ValueElement("value", _Image_component, kwargs=kwargs) del _Image +_Image_component = reacton.core.ComponentWidget(widget=ipywidgets.widgets.widget_media.Image) def _IntProgress( @@ -1657,12 +1629,11 @@ def IntProgress(**kwargs): kwargs["layout"] = Layout(**kwargs["layout"]) if isinstance(kwargs.get("style"), dict): kwargs["style"] = ProgressStyle(**kwargs["style"]) - widget_cls = ipywidgets.widgets.widget_int.IntProgress - comp = reacton.core.ComponentWidget(widget=widget_cls) - return ValueElement("value", comp, kwargs=kwargs) + return ValueElement("value", _IntProgress_component, kwargs=kwargs) del _IntProgress +_IntProgress_component = reacton.core.ComponentWidget(widget=ipywidgets.widgets.widget_int.IntProgress) def _IntRangeSlider( @@ -1726,12 +1697,11 @@ def IntRangeSlider(**kwargs): kwargs["layout"] = Layout(**kwargs["layout"]) if isinstance(kwargs.get("style"), dict): kwargs["style"] = SliderStyle(**kwargs["style"]) - widget_cls = ipywidgets.widgets.widget_int.IntRangeSlider - comp = reacton.core.ComponentWidget(widget=widget_cls) - return ValueElement("value", comp, kwargs=kwargs) + return ValueElement("value", _IntRangeSlider_component, kwargs=kwargs) del _IntRangeSlider +_IntRangeSlider_component = reacton.core.ComponentWidget(widget=ipywidgets.widgets.widget_int.IntRangeSlider) def _IntSlider( @@ -1785,12 +1755,11 @@ def IntSlider(**kwargs): kwargs["layout"] = Layout(**kwargs["layout"]) if isinstance(kwargs.get("style"), dict): kwargs["style"] = SliderStyle(**kwargs["style"]) - widget_cls = ipywidgets.widgets.widget_int.IntSlider - comp = reacton.core.ComponentWidget(widget=widget_cls) - return ValueElement("value", comp, kwargs=kwargs) + return ValueElement("value", _IntSlider_component, kwargs=kwargs) del _IntSlider +_IntSlider_component = reacton.core.ComponentWidget(widget=ipywidgets.widgets.widget_int.IntSlider) def _IntText( @@ -1829,12 +1798,11 @@ def IntText(**kwargs): kwargs["layout"] = Layout(**kwargs["layout"]) if isinstance(kwargs.get("style"), dict): kwargs["style"] = DescriptionStyle(**kwargs["style"]) - widget_cls = ipywidgets.widgets.widget_int.IntText - comp = reacton.core.ComponentWidget(widget=widget_cls) - return ValueElement("value", comp, kwargs=kwargs) + return ValueElement("value", _IntText_component, kwargs=kwargs) del _IntText +_IntText_component = reacton.core.ComponentWidget(widget=ipywidgets.widgets.widget_int.IntText) def _Label( @@ -1871,12 +1839,11 @@ def Label(**kwargs): kwargs["layout"] = Layout(**kwargs["layout"]) if isinstance(kwargs.get("style"), dict): kwargs["style"] = DescriptionStyle(**kwargs["style"]) - widget_cls = ipywidgets.widgets.widget_string.Label - comp = reacton.core.ComponentWidget(widget=widget_cls) - return ValueElement("value", comp, kwargs=kwargs) + return ValueElement("value", _Label_component, kwargs=kwargs) del _Label +_Label_component = reacton.core.ComponentWidget(widget=ipywidgets.widgets.widget_string.Label) def _Layout( @@ -2014,12 +1981,11 @@ def _Layout( @implements(_Layout) def Layout(**kwargs): - widget_cls = ipywidgets.widgets.widget_layout.Layout - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_Layout_component, kwargs=kwargs) del _Layout +_Layout_component = reacton.core.ComponentWidget(widget=ipywidgets.widgets.widget_layout.Layout) def _Output( @@ -2068,12 +2034,11 @@ def func(): def Output(**kwargs): if isinstance(kwargs.get("layout"), dict): kwargs["layout"] = Layout(**kwargs["layout"]) - widget_cls = ipywidgets.widgets.widget_output.Output - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_Output_component, kwargs=kwargs) del _Output +_Output_component = reacton.core.ComponentWidget(widget=ipywidgets.widgets.widget_output.Output) def _Password( @@ -2112,12 +2077,11 @@ def Password(**kwargs): kwargs["layout"] = Layout(**kwargs["layout"]) if isinstance(kwargs.get("style"), dict): kwargs["style"] = DescriptionStyle(**kwargs["style"]) - widget_cls = ipywidgets.widgets.widget_string.Password - comp = reacton.core.ComponentWidget(widget=widget_cls) - return ValueElement("value", comp, kwargs=kwargs) + return ValueElement("value", _Password_component, kwargs=kwargs) del _Password +_Password_component = reacton.core.ComponentWidget(widget=ipywidgets.widgets.widget_string.Password) def _Play( @@ -2166,12 +2130,11 @@ def Play(**kwargs): kwargs["layout"] = Layout(**kwargs["layout"]) if isinstance(kwargs.get("style"), dict): kwargs["style"] = DescriptionStyle(**kwargs["style"]) - widget_cls = ipywidgets.widgets.widget_int.Play - comp = reacton.core.ComponentWidget(widget=widget_cls) - return ValueElement("value", comp, kwargs=kwargs) + return ValueElement("value", _Play_component, kwargs=kwargs) del _Play +_Play_component = reacton.core.ComponentWidget(widget=ipywidgets.widgets.widget_int.Play) def _RadioButtons( @@ -2249,12 +2212,11 @@ def RadioButtons(**kwargs): kwargs["layout"] = Layout(**kwargs["layout"]) if isinstance(kwargs.get("style"), dict): kwargs["style"] = DescriptionStyle(**kwargs["style"]) - widget_cls = ipywidgets.widgets.widget_selection.RadioButtons - comp = reacton.core.ComponentWidget(widget=widget_cls) - return ValueElement("value", comp, kwargs=kwargs) + return ValueElement("value", _RadioButtons_component, kwargs=kwargs) del _RadioButtons +_RadioButtons_component = reacton.core.ComponentWidget(widget=ipywidgets.widgets.widget_selection.RadioButtons) def _Select( @@ -2337,12 +2299,11 @@ def Select(**kwargs): kwargs["layout"] = Layout(**kwargs["layout"]) if isinstance(kwargs.get("style"), dict): kwargs["style"] = DescriptionStyle(**kwargs["style"]) - widget_cls = ipywidgets.widgets.widget_selection.Select - comp = reacton.core.ComponentWidget(widget=widget_cls) - return ValueElement("value", comp, kwargs=kwargs) + return ValueElement("value", _Select_component, kwargs=kwargs) del _Select +_Select_component = reacton.core.ComponentWidget(widget=ipywidgets.widgets.widget_selection.Select) def _SelectMultiple( @@ -2432,12 +2393,11 @@ def SelectMultiple(**kwargs): kwargs["layout"] = Layout(**kwargs["layout"]) if isinstance(kwargs.get("style"), dict): kwargs["style"] = DescriptionStyle(**kwargs["style"]) - widget_cls = ipywidgets.widgets.widget_selection.SelectMultiple - comp = reacton.core.ComponentWidget(widget=widget_cls) - return ValueElement("value", comp, kwargs=kwargs) + return ValueElement("value", _SelectMultiple_component, kwargs=kwargs) del _SelectMultiple +_SelectMultiple_component = reacton.core.ComponentWidget(widget=ipywidgets.widgets.widget_selection.SelectMultiple) def _SelectionRangeSlider( @@ -2542,12 +2502,11 @@ def SelectionRangeSlider(**kwargs): kwargs["layout"] = Layout(**kwargs["layout"]) if isinstance(kwargs.get("style"), dict): kwargs["style"] = DescriptionStyle(**kwargs["style"]) - widget_cls = ipywidgets.widgets.widget_selection.SelectionRangeSlider - comp = reacton.core.ComponentWidget(widget=widget_cls) - return ValueElement("value", comp, kwargs=kwargs) + return ValueElement("value", _SelectionRangeSlider_component, kwargs=kwargs) del _SelectionRangeSlider +_SelectionRangeSlider_component = reacton.core.ComponentWidget(widget=ipywidgets.widgets.widget_selection.SelectionRangeSlider) def _SelectionSlider( @@ -2644,12 +2603,11 @@ def SelectionSlider(**kwargs): kwargs["layout"] = Layout(**kwargs["layout"]) if isinstance(kwargs.get("style"), dict): kwargs["style"] = DescriptionStyle(**kwargs["style"]) - widget_cls = ipywidgets.widgets.widget_selection.SelectionSlider - comp = reacton.core.ComponentWidget(widget=widget_cls) - return ValueElement("value", comp, kwargs=kwargs) + return ValueElement("value", _SelectionSlider_component, kwargs=kwargs) del _SelectionSlider +_SelectionSlider_component = reacton.core.ComponentWidget(widget=ipywidgets.widgets.widget_selection.SelectionSlider) def _SliderStyle( @@ -2667,12 +2625,11 @@ def _SliderStyle( @implements(_SliderStyle) def SliderStyle(**kwargs): - widget_cls = ipywidgets.widgets.widget_int.SliderStyle - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_SliderStyle_component, kwargs=kwargs) del _SliderStyle +_SliderStyle_component = reacton.core.ComponentWidget(widget=ipywidgets.widgets.widget_int.SliderStyle) def _Style() -> Element[ipywidgets.widgets.widget_style.Style]: @@ -2682,12 +2639,11 @@ def _Style() -> Element[ipywidgets.widgets.widget_style.Style]: @implements(_Style) def Style(**kwargs): - widget_cls = ipywidgets.widgets.widget_style.Style - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_Style_component, kwargs=kwargs) del _Style +_Style_component = reacton.core.ComponentWidget(widget=ipywidgets.widgets.widget_style.Style) def _Tab( @@ -2712,12 +2668,11 @@ def _Tab( def Tab(**kwargs): if isinstance(kwargs.get("layout"), dict): kwargs["layout"] = Layout(**kwargs["layout"]) - widget_cls = ipywidgets.widgets.widget_selectioncontainer.Tab - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_Tab_component, kwargs=kwargs) del _Tab +_Tab_component = reacton.core.ComponentWidget(widget=ipywidgets.widgets.widget_selectioncontainer.Tab) def _Text( @@ -2756,12 +2711,11 @@ def Text(**kwargs): kwargs["layout"] = Layout(**kwargs["layout"]) if isinstance(kwargs.get("style"), dict): kwargs["style"] = DescriptionStyle(**kwargs["style"]) - widget_cls = ipywidgets.widgets.widget_string.Text - comp = reacton.core.ComponentWidget(widget=widget_cls) - return ValueElement("value", comp, kwargs=kwargs) + return ValueElement("value", _Text_component, kwargs=kwargs) del _Text +_Text_component = reacton.core.ComponentWidget(widget=ipywidgets.widgets.widget_string.Text) def _Textarea( @@ -2803,12 +2757,11 @@ def Textarea(**kwargs): kwargs["layout"] = Layout(**kwargs["layout"]) if isinstance(kwargs.get("style"), dict): kwargs["style"] = DescriptionStyle(**kwargs["style"]) - widget_cls = ipywidgets.widgets.widget_string.Textarea - comp = reacton.core.ComponentWidget(widget=widget_cls) - return ValueElement("value", comp, kwargs=kwargs) + return ValueElement("value", _Textarea_component, kwargs=kwargs) del _Textarea +_Textarea_component = reacton.core.ComponentWidget(widget=ipywidgets.widgets.widget_string.Textarea) def _ToggleButton( @@ -2862,12 +2815,11 @@ def ToggleButton(**kwargs): kwargs["layout"] = Layout(**kwargs["layout"]) if isinstance(kwargs.get("style"), dict): kwargs["style"] = DescriptionStyle(**kwargs["style"]) - widget_cls = ipywidgets.widgets.widget_bool.ToggleButton - comp = reacton.core.ComponentWidget(widget=widget_cls) - return ValueElement("value", comp, kwargs=kwargs) + return ValueElement("value", _ToggleButton_component, kwargs=kwargs) del _ToggleButton +_ToggleButton_component = reacton.core.ComponentWidget(widget=ipywidgets.widgets.widget_bool.ToggleButton) def _ToggleButtons( @@ -2969,12 +2921,11 @@ def ToggleButtons(**kwargs): kwargs["layout"] = Layout(**kwargs["layout"]) if isinstance(kwargs.get("style"), dict): kwargs["style"] = ToggleButtonsStyle(**kwargs["style"]) - widget_cls = ipywidgets.widgets.widget_selection.ToggleButtons - comp = reacton.core.ComponentWidget(widget=widget_cls) - return ValueElement("value", comp, kwargs=kwargs) + return ValueElement("value", _ToggleButtons_component, kwargs=kwargs) del _ToggleButtons +_ToggleButtons_component = reacton.core.ComponentWidget(widget=ipywidgets.widgets.widget_selection.ToggleButtons) def _ToggleButtonsStyle( @@ -3006,12 +2957,11 @@ def _ToggleButtonsStyle( @implements(_ToggleButtonsStyle) def ToggleButtonsStyle(**kwargs): - widget_cls = ipywidgets.widgets.widget_selection.ToggleButtonsStyle - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_ToggleButtonsStyle_component, kwargs=kwargs) del _ToggleButtonsStyle +_ToggleButtonsStyle_component = reacton.core.ComponentWidget(widget=ipywidgets.widgets.widget_selection.ToggleButtonsStyle) def _TwoByTwoLayout( @@ -3094,12 +3044,11 @@ def _TwoByTwoLayout( def TwoByTwoLayout(**kwargs): if isinstance(kwargs.get("layout"), dict): kwargs["layout"] = Layout(**kwargs["layout"]) - widget_cls = ipywidgets.widgets.widget_templates.TwoByTwoLayout - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_TwoByTwoLayout_component, kwargs=kwargs) del _TwoByTwoLayout +_TwoByTwoLayout_component = reacton.core.ComponentWidget(widget=ipywidgets.widgets.widget_templates.TwoByTwoLayout) def _VBox( @@ -3139,12 +3088,11 @@ def _VBox( def VBox(**kwargs): if isinstance(kwargs.get("layout"), dict): kwargs["layout"] = Layout(**kwargs["layout"]) - widget_cls = ipywidgets.widgets.widget_box.VBox - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_VBox_component, kwargs=kwargs) del _VBox +_VBox_component = reacton.core.ComponentWidget(widget=ipywidgets.widgets.widget_box.VBox) def _Valid( @@ -3187,12 +3135,11 @@ def Valid(**kwargs): kwargs["layout"] = Layout(**kwargs["layout"]) if isinstance(kwargs.get("style"), dict): kwargs["style"] = DescriptionStyle(**kwargs["style"]) - widget_cls = ipywidgets.widgets.widget_bool.Valid - comp = reacton.core.ComponentWidget(widget=widget_cls) - return ValueElement("value", comp, kwargs=kwargs) + return ValueElement("value", _Valid_component, kwargs=kwargs) del _Valid +_Valid_component = reacton.core.ComponentWidget(widget=ipywidgets.widgets.widget_bool.Valid) def _ValueWidget(value: Any = None, on_value: typing.Callable[[Any], Any] = None) -> ValueElement[ipywidgets.widgets.valuewidget.ValueWidget, Any]: @@ -3204,12 +3151,11 @@ def _ValueWidget(value: Any = None, on_value: typing.Callable[[Any], Any] = None @implements(_ValueWidget) def ValueWidget(**kwargs): - widget_cls = ipywidgets.widgets.valuewidget.ValueWidget - comp = reacton.core.ComponentWidget(widget=widget_cls) - return ValueElement("value", comp, kwargs=kwargs) + return ValueElement("value", _ValueWidget_component, kwargs=kwargs) del _ValueWidget +_ValueWidget_component = reacton.core.ComponentWidget(widget=ipywidgets.widgets.valuewidget.ValueWidget) def _Video( @@ -3255,12 +3201,11 @@ def _Video( def Video(**kwargs): if isinstance(kwargs.get("layout"), dict): kwargs["layout"] = Layout(**kwargs["layout"]) - widget_cls = ipywidgets.widgets.widget_media.Video - comp = reacton.core.ComponentWidget(widget=widget_cls) - return ValueElement("value", comp, kwargs=kwargs) + return ValueElement("value", _Video_component, kwargs=kwargs) del _Video +_Video_component = reacton.core.ComponentWidget(widget=ipywidgets.widgets.widget_media.Video) def _interactive( @@ -3301,12 +3246,11 @@ def _interactive( def interactive(**kwargs): if isinstance(kwargs.get("layout"), dict): kwargs["layout"] = Layout(**kwargs["layout"]) - widget_cls = ipywidgets.widgets.interaction.interactive - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_interactive_component, kwargs=kwargs) del _interactive +_interactive_component = reacton.core.ComponentWidget(widget=ipywidgets.widgets.interaction.interactive) def _DescriptionStyle( @@ -3320,12 +3264,11 @@ def _DescriptionStyle( @implements(_DescriptionStyle) def DescriptionStyle(**kwargs): - widget_cls = ipywidgets.widgets.widget_description.DescriptionStyle - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_DescriptionStyle_component, kwargs=kwargs) del _DescriptionStyle +_DescriptionStyle_component = reacton.core.ComponentWidget(widget=ipywidgets.widgets.widget_description.DescriptionStyle) def _DescriptionWidget( @@ -3352,12 +3295,11 @@ def DescriptionWidget(**kwargs): kwargs["layout"] = Layout(**kwargs["layout"]) if isinstance(kwargs.get("style"), dict): kwargs["style"] = DescriptionStyle(**kwargs["style"]) - widget_cls = ipywidgets.widgets.widget_description.DescriptionWidget - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_DescriptionWidget_component, kwargs=kwargs) del _DescriptionWidget +_DescriptionWidget_component = reacton.core.ComponentWidget(widget=ipywidgets.widgets.widget_description.DescriptionWidget) def _ProgressStyle( @@ -3375,12 +3317,11 @@ def _ProgressStyle( @implements(_ProgressStyle) def ProgressStyle(**kwargs): - widget_cls = ipywidgets.widgets.widget_int.ProgressStyle - comp = reacton.core.ComponentWidget(widget=widget_cls) - return Element(comp, kwargs=kwargs) + return Element(_ProgressStyle_component, kwargs=kwargs) del _ProgressStyle +_ProgressStyle_component = reacton.core.ComponentWidget(widget=ipywidgets.widgets.widget_int.ProgressStyle) def __BoundedInt( @@ -3417,12 +3358,11 @@ def _BoundedInt(**kwargs): kwargs["layout"] = Layout(**kwargs["layout"]) if isinstance(kwargs.get("style"), dict): kwargs["style"] = DescriptionStyle(**kwargs["style"]) - widget_cls = ipywidgets.widgets.widget_int._BoundedInt - comp = reacton.core.ComponentWidget(widget=widget_cls) - return ValueElement("value", comp, kwargs=kwargs) + return ValueElement("value", __BoundedInt_component, kwargs=kwargs) del __BoundedInt +__BoundedInt_component = reacton.core.ComponentWidget(widget=ipywidgets.widgets.widget_int._BoundedInt) def __BoundedIntRange( @@ -3458,12 +3398,11 @@ def _BoundedIntRange(**kwargs): kwargs["layout"] = Layout(**kwargs["layout"]) if isinstance(kwargs.get("style"), dict): kwargs["style"] = DescriptionStyle(**kwargs["style"]) - widget_cls = ipywidgets.widgets.widget_int._BoundedIntRange - comp = reacton.core.ComponentWidget(widget=widget_cls) - return ValueElement("value", comp, kwargs=kwargs) + return ValueElement("value", __BoundedIntRange_component, kwargs=kwargs) del __BoundedIntRange +__BoundedIntRange_component = reacton.core.ComponentWidget(widget=ipywidgets.widgets.widget_int._BoundedIntRange) def __Int( @@ -3493,12 +3432,11 @@ def _Int(**kwargs): kwargs["layout"] = Layout(**kwargs["layout"]) if isinstance(kwargs.get("style"), dict): kwargs["style"] = DescriptionStyle(**kwargs["style"]) - widget_cls = ipywidgets.widgets.widget_int._Int - comp = reacton.core.ComponentWidget(widget=widget_cls) - return ValueElement("value", comp, kwargs=kwargs) + return ValueElement("value", __Int_component, kwargs=kwargs) del __Int +__Int_component = reacton.core.ComponentWidget(widget=ipywidgets.widgets.widget_int._Int) def __IntRange( @@ -3528,9 +3466,8 @@ def _IntRange(**kwargs): kwargs["layout"] = Layout(**kwargs["layout"]) if isinstance(kwargs.get("style"), dict): kwargs["style"] = DescriptionStyle(**kwargs["style"]) - widget_cls = ipywidgets.widgets.widget_int._IntRange - comp = reacton.core.ComponentWidget(widget=widget_cls) - return ValueElement("value", comp, kwargs=kwargs) + return ValueElement("value", __IntRange_component, kwargs=kwargs) del __IntRange +__IntRange_component = reacton.core.ComponentWidget(widget=ipywidgets.widgets.widget_int._IntRange) diff --git a/setup_cython.py b/setup_cython.py new file mode 100644 index 0000000..59250a7 --- /dev/null +++ b/setup_cython.py @@ -0,0 +1,60 @@ +"""Optional: compile reacton's hot modules with Cython (pure Python mode). + + pip install "cython>=3.1,<3.2" setuptools + python setup_cython.py build_ext --inplace + +reacton does not need this: the same modules run as plain Python (pip install -e . works +without Cython). With the compiled modules next to the .py files, they are imported instead. +REACTON_CYTHON=0 forces the plain Python modules (reacton/_fastcore_import.py). Remove the +compiled modules with: python setup_cython.py clean_inplace +""" + +import glob +import os +import sys + +from Cython.Build import cythonize +from setuptools import Distribution, Extension +from setuptools.command.build_ext import build_ext + +HERE = os.path.dirname(os.path.abspath(__file__)) +MODULES = ["reacton._fastcore"] + + +def clean_inplace(): + for module in MODULES: + base = module.replace(".", "/") + for path in glob.glob(base + ".*.so") + glob.glob(base + ".*.pyd") + [base + ".c", base + ".html"]: + if os.path.exists(path): + os.remove(path) + print("removed", path) + + +def build_inplace(): + extensions = [Extension(module, [module.replace(".", "/") + ".py"]) for module in MODULES] + ext_modules = cythonize( + extensions, + compiler_directives={ + "language_level": 3, + # compiled functions behave like Python functions (methods bind, inspect.signature) + "binding": True, + }, + annotate=os.environ.get("REACTON_CYTHON_ANNOTATE") == "1", + ) + # (a Distribution of its own: setuptools.setup() would read reacton's hatch pyproject.toml) + dist = Distribution({"name": "reacton-cython-build", "ext_modules": ext_modules}) + command = build_ext(dist) + command.inplace = True + command.ensure_finalized() + command.run() + + +if __name__ == "__main__": + os.chdir(HERE) + args = sys.argv[1:] + if args == ["clean_inplace"]: + clean_inplace() + elif args in (["build_ext", "--inplace"], []): + build_inplace() + else: + sys.exit("usage: python setup_cython.py [build_ext --inplace | clean_inplace]")