"""
Implementation of the app Component classes (LocalComponent,
ProxyComponent, StubComponent), which form the basis for the
PyComponent and JsComponent classes (and their proxies).
"""
import sys
from pscript import window, JSString, this_is_js
from .. import event
from ..event import Component, loop, Dict
from ..event._component import (with_metaclass, ComponentMeta)
from ..event._property import Property
from ..event._emitter import EmitterDescriptor
from ..event._action import ActionDescriptor
from ..event._js import create_js_component_class
from ._asset import get_mod_name
from . import logger
# The clientcore module is a PScript module that forms the core of the
# client-side of Flexx. We import the serializer instance, and can use
# that name in both Python and JS. Of course, in JS it's just the
# corresponding instance from the module that's being used.
# By using something from clientcore in JS here, we make clientcore a
# dependency of the the current module.
from ._clientcore import serializer, bsdf
manager = None # Set by __init__ to prevent circular dependencies
def make_proxy_action(action):
# Note: the flx_prefixes are picked up by the code in flexx.event that
# compiles component classes, so it can fix /insert the name for JS.
flx_name = action._name
def flx_proxy_action(self, *args):
self._proxy_action(flx_name, *args)
return self
flx_proxy_action.__doc__ = action.__doc__
flx_proxy_action.__qualname__ = 'flx_proxy_action'
return flx_proxy_action # ActionDescriptor(flx_proxy_action, flx_name, '')
def make_proxy_emitter(emitter):
# Note: the flx_prefixes are picked up by the code in flexx.event that
# compiles component classes, so it can fix /insert the name for JS.
flx_name = emitter._name
def flx_proxy_emitter(self, *args):
self._proxy_emitter(flx_name, *args)
flx_proxy_emitter.__doc__ = emitter.__doc__
flx_proxy_emitter.__qualname__ = 'flx_proxy_emitter'
return flx_proxy_emitter # EmitterDescriptor(flx_proxy_emitter, flx_name, '')
def get_component_classes():
""" Get a list of all known PyComponent and JsComponent subclasses.
"""
return [c for c in AppComponentMeta.CLASSES]
def meta_repr(cls):
""" A repr function to provide some context on the purpose of a class.
"""
if issubclass(cls, PyComponent):
prefix = 'PyComponent class'
elif issubclass(cls, PyComponent.JS):
prefix = 'proxy PyComponent class for JS '
elif issubclass(cls, JsComponent):
prefix = 'proxy JsComponent class'
elif issubclass(cls, JsComponent.JS):
prefix = 'JsComponent class for JS'
else:
prefix = 'class'
return "<%s '%s.%s'>" % (prefix, cls.__module__, cls.__name__)
class LocalProperty(Property):
""" A generic property that is only present at the local side of
the component, i.e. not at the proxy. Intended for properties that
the other side should not care about, and/or for wich syncing would be
problematic, e.g. for performance or because it contains components
that we want to keep local.
"""
class ComponentMetaJS(ComponentMeta):
""" Meta class for autogenerated classes intended for JavaScript:
Proxy PyComponent and local JsComponents.
"""
__repr__ = meta_repr
def __init__(cls, name, *args):
name = name.encode() if sys.version_info[0] == 2 else name
return super().__init__(name, *args)
class AppComponentMeta(ComponentMeta):
""" Meta class for PyComponent and JsComponent
that generate a matching class for JS.
"""
# Keep track of all subclasses
CLASSES = []
__repr__ = meta_repr
def _init_hook1(cls, cls_name, bases, dct):
# cls is the class to be
# cls.__dict__ is its current dict, which may contain inherited items
# dct is the dict represented by exactly this class (no inheritance)
# Get CSS from the class now
CSS = dct.get('CSS', '')
# Create corresponding class for JS
if issubclass(cls, LocalComponent):
cls._make_js_proxy_class(cls_name, bases, dct)
elif issubclass(cls, ProxyComponent):
cls._make_js_local_class(cls_name, bases, dct)
else: # pragma: no cover
raise TypeError('Expected class to inherit from '
'LocalComponent or ProxyComponent.')
# Write __jsmodule__; an optimization for our module/asset system
cls.__jsmodule__ = get_mod_name(sys.modules[cls.__module__])
cls.JS.__jsmodule__ = cls.__jsmodule__ # need it in JS too
cls.JS.__module__ = cls.__module__
# Set CSS
cls.CSS = CSS
try:
delattr(cls.JS, 'CSS')
except AttributeError:
pass
def _init_hook2(cls, cls_name, bases, dct):
# Set __proxy_properties__ and __emitters__
if issubclass(cls, LocalComponent):
cls.__proxy_properties__ = cls.JS.__properties__
cls.JS.__emitters__ = cls.__emitters__
else:
cls.JS.__proxy_properties__ = cls.__properties__
cls.__emitters__ = cls.JS.__emitters__
# Set JS on the JS class
cls.JS.CODE = cls._get_js()
# Register this class. The classes in this list will be automatically
# "pushed to JS" in a JIT fashion. We have to make sure that we include
# the code for base classes not in this list, which we do in _get_js().
AppComponentMeta.CLASSES.append(cls)
def _make_js_proxy_class(cls, cls_name, bases, dct):
for c in bases:
assert not issubclass(cls, ProxyComponent)
# Fix inheritance for JS variant
jsbases = [getattr(b, 'JS') for b in cls.__bases__ if hasattr(b, 'JS')]
if not jsbases:
jsbases.append(ProxyComponent)
jsdict = {}
# Copy properties from this class to the JS proxy class.
# in Python 3.6 we iterate in the order in which the items are defined,
for name, val in dct.items():
if name.startswith('__') and name.endswith('__'):
continue
elif isinstance(val, LocalProperty):
pass # do not copy over
elif isinstance(val, Property):
jsdict[name] = val # properties are the same
elif isinstance(val, EmitterDescriptor):
jsdict[name] = make_proxy_emitter(val) # proxy emitter
elif isinstance(val, ActionDescriptor):
jsdict[name] = make_proxy_action(val) # proxy actions
else:
pass # no reactions/functions/class attributes on the proxy side
# Create JS class
cls.JS = ComponentMetaJS(cls_name, tuple(jsbases), jsdict)
def _make_js_local_class(cls, cls_name, bases, dct):
for c in bases:
assert not issubclass(cls, LocalComponent)
# Fix inheritance for JS variant
jsbases = [getattr(b, 'JS') for b in cls.__bases__ if hasattr(b, 'JS')]
if not jsbases:
jsbases.append(LocalComponent)
jsdict = {}
# Names that should stay in Python in addition to magic methods
py_only = ['_repr_html_']
# Copy properties from this class to the JS proxy class.
# in Python 3.6 we iterate in the order in which the items are defined,
for name, val in list(dct.items()):
# Skip?
if isinstance(val, classmethod):
continue
elif name in py_only or name.startswith('__') and name.endswith('__'):
if name not in ('__init__', '__linenr__'):
continue
# Move over to JS
if (isinstance(val, Property) or (callable(val) and
name.endswith('_validate'))):
jsdict[name] = val # properties are the same
if isinstance(val, LocalProperty):
delattr(cls, name)
dct.pop(name, None)
elif isinstance(val, EmitterDescriptor):
# JS part gets the proper emitter, Py side gets a proxy
jsdict[name] = val
setattr(cls, name, make_proxy_emitter(val))
elif isinstance(val, ActionDescriptor):
# JS part gets the proper action, Py side gets a proxy
jsdict[name] = val
setattr(cls, name, make_proxy_action(val))
else:
# Move attribute from the Py class to the JS class
jsdict[name] = val
delattr(cls, name)
dct.pop(name, None) # is this necessary?
# Create JS class
cls.JS = ComponentMetaJS(cls_name, tuple(jsbases), jsdict)
def _get_js(cls):
""" Get source code for this class plus the meta info about the code.
"""
# Since classes are defined in a module, we can safely name the classes
# by their plain name.
cls_name = cls.__name__
base_class = cls.JS.mro()[1]
base_class_name = '%s.prototype' % base_class.__name__
code = []
# Add this class
c = create_js_component_class(cls.JS, cls_name, base_class_name)
meta = c.meta
code.append(c)
# code.append(c.replace('var %s =' % cls_name,
# 'var %s = flexx.classes.%s =' % (cls_name, cls_name), 1))
# Add JS version of the base classes - but only once
if cls.__name__ == 'JsComponent':
c = cls._get_js_of_base_classes()
for k in ['vars_unknown', 'vars_global', 'std_functions', 'std_methods']:
meta[k].update(c.meta[k])
code.insert(0, c)
# Return with meta info
js = JSString('\n'.join(code))
js.meta = meta
return js
def _get_js_of_base_classes(cls):
""" Get JS for BaseAppComponent, LocalComponent, and ProxyComponent.
"""
c1 = create_js_component_class(BaseAppComponent, 'BaseAppComponent',
'Component.prototype')
c2 = create_js_component_class(LocalComponent, 'LocalComponent',
'BaseAppComponent.prototype')
c3 = create_js_component_class(ProxyComponent, 'ProxyComponent',
'BaseAppComponent.prototype')
c4 = create_js_component_class(StubComponent, 'StubComponent',
'BaseAppComponent.prototype')
meta = c1.meta
for k in ['vars_unknown', 'vars_global', 'std_functions', 'std_methods']:
for c in (c2, c3, c4):
meta[k].update(c.meta[k])
js = JSString('\n'.join([c1, c2, c3, c4]))
js.meta = meta
return js
class BaseAppComponent(Component):
""" Inherits from :class:`Component <flexx.event.Component>`
Abstract class for Component classes that can be "shared" between
Python and JavaScript. The concrete implementations are:
* The ``PyComponent`` class, which operates in Python, but has a proxy
object in JavaSript to which properties are synced and from which actions
can be invoked.
* The ``JsComponent`` class, which operates in JavaScript, but can have a proxy
object in Python to which properties are synced and from which actions
can be invoked.
* The ``StubComponent`` class, which represents a component class that is
somewhere else, perhaps in another session. It does not have any
properties, nor actions. But it can be "moved around".
"""
session = event.Attribute(doc="""
The session to which this component belongs. The component id
is unique within its session.
""")
root = event.Attribute(doc="""
The component that represents the root of the application. Alias for
session.app.
""")
uid = event.Attribute(doc="""
A unique identifier for this component; a combination of the
session and component id's.
""")
def _comp_init_app_component(self, property_values):
# Pop special attribute
property_values.pop('flx_is_app', None)
# Pop and apply id if given
custom_id = property_values.pop('flx_id', None)
# Pop session or derive from active component
self._session = None
session = property_values.pop('flx_session', None)
if session is not None:
self._session = session
else:
active = loop.get_active_components() # Note that self is active too
active = active[-2] if len(active) > 1 else None
if active is not None:
self._session = active._session
else:
if not this_is_js():
self._session = manager.get_default_session()
# Register this component with the session (sets _id and _uid)
if self._session is None:
raise RuntimeError('%s needs a session!' % (custom_id or self._id))
self._session._register_component(self, custom_id)
self._root = self._session.app
# Return whether this instance was instantiated locally
return custom_id is None
class LocalComponent(BaseAppComponent):
"""
Base class for PyComponent in Python and JsComponent in JavaScript.
"""
def _comp_init_property_values(self, property_values):
# This is a good time to register with the session, and
# instantiate the proxy class. Property values have been set at this
# point, but init() has not yet been called.
# Keep track of what events are registered at the proxy
self.__event_types_at_proxy = []
# Init more
self._comp_init_app_component(property_values) # pops items
# Pop whether this local instance has a proxy at the other side
self._has_proxy = property_values.pop('flx_has_proxy', False)
# Call original method
super()._comp_init_property_values(property_values)
if this_is_js():
# This is a local JsComponent in JavaScript
self._event_listeners = []
else:
# This is a local PyComponent in Python
# A PyComponent always has a corresponding proxy in JS
self._ensure_proxy_instance(False)
def _ensure_proxy_instance(self, include_props=True):
""" Make the other end instantiate a proxy if necessary. This is e.g.
called by the BSDF serializer when a LocalComponent gets serialized.
A PyComponent always has a Proxy component, and we should not
dispose or delete it until the local component is disposed.
A JsComponent may be instantiated (as its proxy) from Python, in which
case we receive the flx_has_proxy kwarg. Still, Python can "loose" the
proxy class. To ensure that it exists in Python when needed, the BSDF
serializer will ensure it (by calling this method) when it gets
serialized.
In certain cases, it might be that the other end *does* have a proxy
while this end's _has_proxy is False. In that case the INSTANTIATE
command is send, but when handled, will be a no-op.
In certain cases, it might be that the other end just lost its
reference; this end's _has_proxy is True, and a new reference to this
component will fail to resolve. This is countered by keeping hold
of JsComponent proxy classes for at least one roundtrip (upon
initialization as well as disposal).
"""
if self._has_proxy is False and self._disposed is False:
if self._session.status > 0:
props = {}
if include_props:
for name in self.__proxy_properties__:
props[name] = getattr(self, name)
self._session.send_command('INSTANTIATE', self.__jsmodule__,
self.__class__.__name__,
self._id, [], props)
self._has_proxy = True
def emit(self, type, info=None):
# Overload emit() to send events to the proxy object at the other end
ev = super().emit(type, info)
if self._has_proxy is True and self._session.status > 0:
# implicit: and self._disposed is False:
if type in self.__proxy_properties__:
self._session.send_command('INVOKE', self._id, '_emit_at_proxy', [ev])
elif type in self.__event_types_at_proxy:
self._session.send_command('INVOKE', self._id, '_emit_at_proxy', [ev])
def _dispose(self):
# Let proxy side know that we no longer exist, and that it should
# dispose too. Send regardless of whether we have a proxy!
was_disposed = self._disposed
super()._dispose()
self._has_proxy = False # because we will tell it to dispose
if was_disposed is False and self._session is not None:
self._session._unregister_component(self)
if self._session.status > 0:
self._session.send_command('DISPOSE', self._id)
def _flx_set_has_proxy(self, has_proxy):
self._has_proxy = has_proxy
def _flx_set_event_types_at_proxy(self, event_types):
self.__event_types_at_proxy = event_types
class ProxyComponent(BaseAppComponent):
"""
Base class for JSComponent in Python and PyComponent in JavaScript.
"""
def __init__(self, *init_args, **kwargs):
# Need to overload this to handle init_args
if this_is_js():
# This is a proxy PyComponent in JavaScript.
# Always instantiated via an INSTANTIATE command from Python.
assert len(init_args) == 0
if 'flx_id' not in kwargs:
raise RuntimeError('Cannot instantiate a PyComponent from JS.')
super().__init__(**kwargs)
else:
# This is a proxy JsComponent in Python.
# Can be instantiated in Python,
self._flx_init_args = init_args
super().__init__(**kwargs)
def _comp_init_property_values(self, property_values):
# Init more
local_inst = self._comp_init_app_component(property_values) # pops items
# Call original method, only set props if this is instantiated "by the local"
props2set = {} if local_inst else property_values
super()._comp_init_property_values(props2set)
if this_is_js():
# This is a proxy PyComponent in JavaScript
assert len(property_values.keys()) == 0
else:
# This is a proxy JsComponent in Python
# Instantiate JavaScript version of this class
if local_inst is True: # i.e. only if Python "instantiated" it
property_values['flx_has_proxy'] = True
active_components = [c for c in loop.get_active_components()[:-1]
if isinstance(c, (PyComponent, JsComponent))]
self._session.send_command('INSTANTIATE', self.__jsmodule__,
self.__class__.__name__, self._id,
self._flx_init_args, property_values,
active_components)
del self._flx_init_args
def _comp_apply_property_values(self, values):
# Apply props in silence
for name, value in values:
setattr(self, '_' + name + '_value', value)
def _proxy_action(self, name, *args, **kwargs):
""" To invoke actions on the real object.
"""
assert not kwargs
# if self._session.status > 0, mmm, or rather error?
self._session.send_command('INVOKE', self._id, name, args)
def _proxy_emitter(self, name, *args, **kwargs):
""" To handle use of placeholder emitters.
"""
# todo: I am not sure yet whether to allow or disallow it. We disallow now;
# we can always INVOKE the emitter at the other side if that proves needed
if this_is_js():
logger.error('Cannot use emitters of a PyComponent in JS.')
else:
logger.error('Cannot use emitters of a JsComponent in Py.')
def _mutate(self, *args, **kwargs): # pragma: no cover
""" Disable mutations on the proxy class.
"""
raise RuntimeError('Cannot mutate properties from a proxy class.')
# Reference objects to get them collected into the JS variant of this
# module. Do it here, in a place where it wont hurt.
serializer # to bring in _clientcore as a way of bootstrapping
BsdfComponentExtension
def _registered_reactions_hook(self):
""" Keep the local component informed about what event types this proxy
is interested in. This way, the trafic can be minimized, e.g. not send
mouse move events if they're not used anyway.
"""
event_types = super()._registered_reactions_hook()
try:
if self._disposed is False and self._session.status > 0:
self._session.send_command('INVOKE', self._id,
'_flx_set_event_types_at_proxy',
[event_types])
finally:
return event_types
@event.action
def _emit_at_proxy(self, ev):
""" Action used by the local component to push an event to the proxy
component. If the event represents a property-update, the mutation
is applied, otherwise the event is emitted here.
"""
if not this_is_js():
ev = Dict(ev)
if ev.type in self.__properties__ and hasattr(ev, 'mutation'):
# Mutate the property - this will cause an emit
if ev.mutation == 'set':
super()._mutate(ev.type, ev.new_value)
else:
super()._mutate(ev.type, ev.objects, ev.mutation, ev.index)
else:
self.emit(ev.type, ev)
def dispose(self):
if this_is_js():
# The server is leading ...
raise RuntimeError('Cannot dispose a PyComponent from JS.')
else:
# Disposing a JsComponent from JS is like invoking an action;
# we don't actually dispose ourselves just yet.
if self._session.status > 0:
self._session.send_command('INVOKE', self._id, 'dispose', [])
else:
super().dispose()
def _dispose(self):
# This gets called by the session upon a DISPOSE command,
# or on Python from __delete__ (via call_soon).
was_disposed = self._disposed
super()._dispose()
if was_disposed is False and self._session is not None:
self._session._unregister_component(self)
if self._session.status > 0:
# Let other side know that we no longer exist.
self._session.send_command('INVOKE', self._id,
'_flx_set_has_proxy', [False])
class StubComponent(BaseAppComponent):
"""
Class to represent stub proxy components to take the place of components
that do not belong to the current session, or that do not exist
for whatever reason. These objects cannot really be used, but they can
be moved around.
"""
def __init__(self, session, id):
super().__init__()
self._session = session
self._id = id
self._uid = session.id + '_' + id
def __repr__(self):
return ("<StubComponent for '%s' in session '%s' at 0x%x>" %
(self._id, self._session.id, id(self)))
# LocalComponent and ProxyComponent need __jsmodule__, but they do not
# participate in the AppComponentMeta class, so we add it here.
LocalComponent.__jsmodule__ = __name__
ProxyComponent.__jsmodule__ = __name__
StubComponent.__jsmodule__ = __name__
class JsComponent(with_metaclass(AppComponentMeta, ProxyComponent)):
""" Inherits from :class:`BaseAppComponent <flexx.app.BaseAppComponent>`
Base component class that operates in JavaScript, but is accessible
in Python, where its properties and events can be observed,
and actions can be invoked.
JsComponents can be instantiated from both JavaScript and Python. A
corresponding proxy component is not necessarily present in Python. It
is created automatically when needed (e.g. when referenced by a property).
A JsComponent can be explicitly disposed from both Python and JavaScript.
When the Python garbage collector collects a JsComponent (or really, the
proxy thereof), only the Python side proxy is disposed; the JsComponent
in JS itself will be unaffected. Make sure to call ``dispose()`` when
needed!
"""
# The meta class will generate a JsComponent local class for JS
# and move all props, actions, etc. to it.
def __repr__(self):
d = ' (disposed)' if self._disposed else ''
return "<JsComponent '%s'%s at 0x%x>" % (self._id, d, id(self))
def _addEventListener(self, node, type, callback, capture=False):
""" Register events with DOM nodes, to be automatically cleaned up
when this object is disposed.
"""
node.addEventListener(type, callback, capture)
self._event_listeners.push((node, type, callback, capture))
def _dispose(self):
super()._dispose()
while len(self._event_listeners) > 0:
try:
node, type, callback, capture = self._event_listeners.pop()
node.removeEventListener(type, callback, capture)
except Exception as err:
print(err)
# Note: positioned below JSComponent, because linenr is used to sort JS defs,
# and the JS for the base component classes is attached to JSComponent.
class PyComponent(with_metaclass(AppComponentMeta, LocalComponent)):
""" Inherits from :class:`BaseAppComponent <flexx.app.BaseAppComponent>`
Base component class that operates in Python, but is accessible
in JavaScript, where its properties and events can be observed,
and actions can be invoked.
PyComponents can only be instantiated in Python, and always have
a corresponding proxy object in JS. PyComponents can be disposed only
from Python. Disposal also happens if the Python garbage collector
collects a PyComponent.
"""
# The meta class generates a PyComponent proxy class for JS.
def __repr__(self):
d = ' (disposed)' if self._disposed else ''
return "<PyComponent '%s'%s at 0x%x>" % (self._id, d, id(self))
class BsdfComponentExtension(bsdf.Extension):
""" A BSDF extension to encode flexx.app Component objects based on their
session id and component id.
"""
name = 'flexx.app.component'
cls = BaseAppComponent # PyComponent, JsComponent, StubComponent
def match(self, s, c):
# This is actually the default behavior, but added for completenes
return isinstance(c, self.cls)
def encode(self, s, c):
if isinstance(c, PyComponent): # i.e. LocalComponent in Python
c._ensure_proxy_instance()
return dict(session_id=c._session.id, id=c._id)
def decode(self, s, d):
c = None
session = manager.get_session_by_id(d['session_id'])
if session is None:
# object from other session
session = object()
session.id = d['session_id']
c = StubComponent(session, d['id'])
else:
c = session.get_component_instance(d['id'])
if c is None: # This should probably not happen
logger.warning('Using stub component for %s.' % d['id'])
c = StubComponent(session, d['id'])
else:
# Keep it alive for a bit
session.keep_alive(c)
return c
# The name and below methods get collected to produce a JS BSDF extension
def match_js(self, s, c): # pragma: no cover
return isinstance(c, BaseAppComponent)
def encode_js(self, s, c): # pragma: no cover
if isinstance(c, JsComponent): # i.e. LocalComponent in JS
c._ensure_proxy_instance()
return dict(session_id=c._session.id, id=c._id)
def decode_js(self, s, d): # pragma: no cover
c = None
session = window.flexx.sessions.get(d['session_id'], None)
if session is None:
session = dict(id=d['session_id'])
c = StubComponent(session, d['id'])
else:
c = session.get_component_instance(d['id'])
if c is None:
logger.warning('Using stub component for %s.' % d['id'])
c = StubComponent(session, d['id'])
return c
# todo: can the mechanism for defining BSDF extensions be simplified? (issue #429)
# Add BSDF extension for serializing components. The JS variant of the
# serializer is added by referencing the extension is JS code.
serializer.add_extension(BsdfComponentExtension)