Example #1
0
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))
Example #2
0
File: _js.py Project: zoofIO/flexx
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))