def create_js_component_class(cls, cls_name, base_class='Component.prototype'): """ Create the JS equivalent of a subclass of the Component class. Given a Python class with actions, properties, emitters and reactions, this creates the code for the JS version of the class. It also supports class constants that are int/float/str, or a tuple/list thereof. The given class does not have to be a subclass of Component. This more or less does what ComponentMeta does, but for JS. """ assert cls_name != 'Component' # we need this special class above instead # Collect meta information of all code pieces that we collect mc = MetaCollector(cls) mc.meta['std_functions'].add( 'op_instantiate') # b/c we use get_class_definition total_code = [] funcs_code = [] # functions and emitters go below class constants const_code = [] err = ('Objects on JS Component classes can only be int, float, str, ' 'or a list/tuple thereof. Not %s -> %r.') total_code.append('\n'.join(get_class_definition(cls_name, base_class)).rstrip()) prefix = '' if cls_name.count('.') else 'var ' total_code[0] = prefix + total_code[0] prototype_prefix = '$' + cls_name.split('.')[-1] + '.' total_code.append('var %s = %s.prototype;' % (prototype_prefix[:-1], cls_name)) # Process class items in original order or sorted by name if we cant class_items = cls.__dict__.items() if sys.version_info < (3, 6): # pragma: no cover class_items = sorted(class_items) for name, val in class_items: if isinstance(val, ActionDescriptor): # Set underlying function as class attribute. This is overwritten # by the instance, but this way super() works. funcname = name # Add function def code = mc.py2js(val._func, prototype_prefix + funcname) code = code.replace('super()', base_class) # fix super # Tweak if this was an autogenerated action # we use flx_ prefixes to indicate autogenerated functions if val._func.__name__.startswith('flx_'): subname = name if name.startswith('set_') or name.startswith('_set_'): subname = name[4:] code = code.replace("flx_name", "'%s'" % subname) code = code.replace('function (', 'function %s (' % val._func.__name__) funcs_code.append(code.rstrip()) # Mark to not bind the func funcs_code.append(prototype_prefix + funcname + '.nobind = true;') funcs_code.append('') elif isinstance(val, ReactionDescriptor): funcname = name # funcname is simply name, so that super() works # Add function def code = mc.py2js(val._func, prototype_prefix + funcname) code = code.replace('super()', base_class) # fix super funcs_code.append(code.rstrip()) # Mark to not bind the func funcs_code.append(prototype_prefix + funcname + '.nobind = true;') # Add mode and connection strings funcs_code.append(prototype_prefix + funcname + '._mode = ' + reprs(val._mode)) if val._connection_strings: funcs_code.append(prototype_prefix + funcname + '._connection_strings = ' + reprs(val._connection_strings)) funcs_code.append('') elif isinstance(val, EmitterDescriptor): funcname = name # Add function def code = mc.py2js(val._func, prototype_prefix + funcname) code = code.replace('super()', base_class) # fix super funcs_code.append(code.rstrip()) # Mark to not bind the func funcs_code.append(prototype_prefix + funcname + '.nobind = true;') funcs_code.append('') elif isinstance(val, Attribute): pass elif isinstance(val, Property): # Mutator and validator functions are picked up as normal functions. # Set default value on class. default_val = json.dumps(val._default) t = '%s_%s_value = %s;' const_code.append(t % (prototype_prefix, name, default_val)) elif isinstance(val, classmethod): pass # ignore, like magics elif (name.startswith('__') and name not in OK_MAGICS and not name.endswith('_validate')): # These are only magics, since class attributes with double-underscores # have already been mangled. Note that we need to exclude validator # funcs of private properties though. pass elif (name.endswith('_validate') and hasattr(val, '__self__') and isinstance(val.__self__, Property)): # Proxy the validator functions (not inline). prop_class = val.__self__.__class__ mod_name_parts = prop_class.__module__.split('.') module_ns = sys.modules[cls.__module__].__dict__ # Get canonical class name, included part of the module name, as # needed, depending on what names exist in the component module. prop_class_name = prop_class.__name__ if prop_class_name not in module_ns: if 'flx' in module_ns and mod_name_parts[0] == 'flexx': prop_class_name = 'flx.' + prop_class_name else: for ip in reversed(range(0, len(mod_name_parts))): if mod_name_parts[ip] in module_ns: m = sys.modules['.'.join(mod_name_parts[:ip + 1])] if m is module_ns[mod_name_parts[ip]]: for ip2 in range(ip, len(mod_name_parts)): m = sys.modules['.'.join( mod_name_parts[:ip2 + 1])] if getattr(m, prop_class_name, None) is prop_class: break prop_class_name = ( '.'.join(mod_name_parts[ip:ip2 + 1]) + '.' + prop_class_name) break # Create function that calls the _validate function t = ' = function (value) { return %s(value, %s, %s); }\n' code = prototype_prefix + name + t % ( prop_class_name + '.prototype._validate', json.dumps( name[1:-9]), json.dumps(val.__self__._data)) funcs_code.append(code) mc.meta['vars_unknown'].add(prop_class_name) elif callable(val): # Functions, including methods attached by the meta class code = mc.py2js(val, prototype_prefix + name) code = code.replace('super()', base_class) # fix super if val.__name__.startswith('flx_'): subname = name[8:] if name.startswith('_mutate_') else name code = code.replace("flx_name", "'%s'" % subname) funcs_code.append(code.rstrip()) funcs_code.append('') else: # Static simple (json serializable) attributes, e.g. __actions__ etc. try: serialized = json.dumps(val) except Exception as err: # noqa flake wtf - pragma: no cover raise ValueError('Attributes on JS Component class must be ' 'JSON compatible.\n%s' % str(err)) const_code.append(prototype_prefix + name + ' = ' + serialized) if const_code: total_code.append('') total_code.extend(const_code) if funcs_code: total_code.append('') total_code.extend(funcs_code) total_code.append('') # Return string with meta info (similar to what py2js returns) mc.meta['vars_unknown'].discard('flx_name') return mc.attach_meta('\n'.join(total_code))
def create_js_component_class(cls, cls_name, base_class='Component.prototype'): """ Create the JS equivalent of a subclass of the Component class. Given a Python class with actions, properties, emitters and reactions, this creates the code for the JS version of the class. It also supports class constants that are int/float/str, or a tuple/list thereof. The given class does not have to be a subclass of Component. This more or less does what ComponentMeta does, but for JS. """ assert cls_name != 'Component' # we need this special class above instead # Collect meta information of all code pieces that we collect mc = MetaCollector(cls) mc.meta['std_functions'].add('op_instantiate') # b/c we use get_class_definition total_code = [] funcs_code = [] # functions and emitters go below class constants const_code = [] err = ('Objects on JS Component classes can only be int, float, str, ' 'or a list/tuple thereof. Not %s -> %r.') total_code.append('\n'.join(get_class_definition(cls_name, base_class)).rstrip()) prefix = '' if cls_name.count('.') else 'var ' total_code[0] = prefix + total_code[0] prototype_prefix = '$' + cls_name.split('.')[-1] + '.' total_code.append('var %s = %s.prototype;' % (prototype_prefix[:-1], cls_name)) # Process class items in original order or sorted by name if we cant class_items = cls.__dict__.items() if sys.version_info < (3, 6): # pragma: no cover class_items = sorted(class_items) for name, val in class_items: if isinstance(val, ActionDescriptor): # Set underlying function as class attribute. This is overwritten # by the instance, but this way super() works. funcname = name # Add function def code = mc.py2js(val._func, prototype_prefix + funcname) code = code.replace('super()', base_class) # fix super # Tweak if this was an autogenerated action # we use flx_ prefixes to indicate autogenerated functions if val._func.__name__.startswith('flx_'): subname = name if name.startswith('set_') or name.startswith('_set_'): subname = name[4:] code = code.replace("flx_name", "'%s'" % subname) code = code.replace('function (', 'function %s (' % val._func.__name__) funcs_code.append(code.rstrip()) # Mark to not bind the func funcs_code.append(prototype_prefix + funcname + '.nobind = true;') funcs_code.append('') elif isinstance(val, ReactionDescriptor): funcname = name # funcname is simply name, so that super() works # Add function def code = mc.py2js(val._func, prototype_prefix + funcname) code = code.replace('super()', base_class) # fix super funcs_code.append(code.rstrip()) # Mark to not bind the func funcs_code.append(prototype_prefix + funcname + '.nobind = true;') # Add mode and connection strings funcs_code.append(prototype_prefix + funcname + '._mode = ' + reprs(val._mode)) if val._connection_strings: funcs_code.append(prototype_prefix + funcname + '._connection_strings = ' + reprs(val._connection_strings)) funcs_code.append('') elif isinstance(val, EmitterDescriptor): funcname = name # Add function def code = mc.py2js(val._func, prototype_prefix + funcname) code = code.replace('super()', base_class) # fix super funcs_code.append(code.rstrip()) # Mark to not bind the func funcs_code.append(prototype_prefix + funcname + '.nobind = true;') funcs_code.append('') elif isinstance(val, Attribute): pass elif isinstance(val, Property): # Mutator and validator functions are picked up as normal functions. # Set default value on class. default_val = json.dumps(val._default) t = '%s_%s_value = %s;' const_code.append(t % (prototype_prefix, name, default_val)) elif isinstance(val, classmethod): pass # ignore, like magics elif (name.startswith('__') and name not in OK_MAGICS and not name.endswith('_validate')): # These are only magics, since class attributes with double-underscores # have already been mangled. Note that we need to exclude validator # funcs of private properties though. pass elif (name.endswith('_validate') and hasattr(val, '__self__') and isinstance(val.__self__, Property)): # Proxy the validator functions (not inline). prop_class = val.__self__.__class__ mod_name_parts = prop_class.__module__.split('.') module_ns = sys.modules[cls.__module__].__dict__ # Get canonical class name, included part of the module name, as # needed, depending on what names exist in the component module. prop_class_name = prop_class.__name__ if prop_class_name not in module_ns: if 'flx' in module_ns and mod_name_parts[0] == 'flexx': prop_class_name = 'flx.' + prop_class_name else: for ip in reversed(range(0, len(mod_name_parts))): if mod_name_parts[ip] in module_ns: m = sys.modules['.'.join(mod_name_parts[:ip+1])] if m is module_ns[mod_name_parts[ip]]: for ip2 in range(ip, len(mod_name_parts)): m = sys.modules['.'.join(mod_name_parts[:ip2+1])] if getattr(m, prop_class_name, None) is prop_class: break prop_class_name = ('.'.join(mod_name_parts[ip:ip2+1]) + '.' + prop_class_name) break # Create function that calls the _validate function t = ' = function (value) { return %s(value, %s, %s); }\n' code = prototype_prefix + name + t % ( prop_class_name + '.prototype._validate', json.dumps(name[1:-9]), json.dumps(val.__self__._data)) funcs_code.append(code) mc.meta['vars_unknown'].add(prop_class_name) elif callable(val): # Functions, including methods attached by the meta class code = mc.py2js(val, prototype_prefix + name) code = code.replace('super()', base_class) # fix super if val.__name__.startswith('flx_'): subname = name[8:] if name.startswith('_mutate_') else name code = code.replace("flx_name", "'%s'" % subname) funcs_code.append(code.rstrip()) funcs_code.append('') else: # Static simple (json serializable) attributes, e.g. __actions__ etc. try: serialized = json.dumps(val) except Exception as err: # noqa flake wtf - pragma: no cover raise ValueError('Attributes on JS Component class must be ' 'JSON compatible.\n%s' % str(err)) const_code.append(prototype_prefix + name + ' = ' + serialized) if const_code: total_code.append('') total_code.extend(const_code) if funcs_code: total_code.append('') total_code.extend(funcs_code) total_code.append('') # Return string with meta info (similar to what py2js returns) mc.meta['vars_unknown'].discard('flx_name') return mc.attach_meta('\n'.join(total_code))