Repository: zoofIO/flexx (python)

Issue:
Make writing functions with verbatim JS more explicit
As it is now, PyScript allows defining the body of a function/method with verbatim JS by only giving that function a docstring. Writing `pass`  or `return` in the body (not in the docstring) will disable it already. Although useful, it can also be the source of frustration if you write a stub method, e.g. for a base class or during development. Therefore I'd like this to be more explicit.

I'd like to see if we can combine this with the ideas for unpythonic PyScript:
```py
with PURE_JS:
    # code here is transpiled, but without the pythonic features like
    # operator overloading, boolean tests, etc.
    for i in range(10):
        if a:
            b = a + i

with RAW_JS:  # or maybe PURE_JS too? If content is only a multiline string, will use it verbatim
    """
    Write your verbatim JS here.
    """
```

The current behavior will continue to exist (for at least a while), but a warning will be produced.

Pull request description:
Add RawJS construct for embedding raw JS and define global instances
Fixes #279, possibly makes #275 easier, helps #104 while honoring #259.

Write raw JS:
```py
RawJS('$')(self.node).datepicker()

...

def foo(x):
    res = []
    RawJS("""
        for (i=0; i<10; i++) {
            ...
        }
    """)
```

Define globals in app, ui:
```py
# in m1.py
some_module = RawJS("require('some.module')")
foo = RawJS("[]")

# in m2.py
from m1 import foo  # hooray, we can share a constant in JS!
```

Patch:
diff --git a/docs/pyscript/api.rst b/docs/pyscript/api.rst
index 32fd301..a7840fc 100644
--- a/docs/pyscript/api.rst
+++ b/docs/pyscript/api.rst
@@ -26,11 +26,14 @@ get closer to the metal by using and/or extending the parser class.
 
 ----
 
+PyScript allows embedding raw JavaScript using the ``RawJS`` class.
+
+.. autoclass:: flexx.pyscript.RawJS
+
+----
+
 The PyScript module has a few dummy constants that can be imported and
 used in your code to let e.g. pyflakes know that the variable exists. E.g.
-``from flexx.pyscript import undefined, window``.
-
-.. data:: undefined
-.. data:: window
-.. data:: Infinity
-.. data:: NaN
+``from flexx.pyscript import undefined, window Infinity, NaN``.
+Arbitrary dummy variables can be imported using
+``from flexx.pyscript.stubs import JSON, foo, bar``.
diff --git a/flexx/app/clientcore.py b/flexx/app/clientcore.py
index 473f4b3..9146b57 100644
--- a/flexx/app/clientcore.py
+++ b/flexx/app/clientcore.py
@@ -2,7 +2,7 @@
 The client's core Flexx engine, implemented in PyScript.
 """
 
-from ..pyscript import this_is_js
+from ..pyscript import this_is_js, RawJS
 from ..pyscript.stubs import window, undefined, require
 
 # This module gets transpiled to JavaScript as a whole
@@ -84,7 +84,7 @@ class Flexx:
             return self.instances[id]
     
     def spin(self, text='*'):
-        """
+        RawJS("""
         if (!window.document.body) {return;}
         var el = window.document.body.children[0];
         if (el && el.classList.contains("flx-spinner")) {
@@ -94,7 +94,7 @@ class Flexx:
                 el.children[0].innerHTML += text.replace(/\*/g, '&#9632');
             }
         }
-        """
+        """)
     
     def initSocket(self):
         """ Make the connection to Python.
@@ -253,7 +253,7 @@ class Flexx:
 
 
 def decodeUtf8(arrayBuffer):
-    """
+    RawJS("""
     var result = "",
         i = 0,
         c = 0,
@@ -293,7 +293,7 @@ def decodeUtf8(arrayBuffer):
         }
     }
     return result;
-    """
+    """)
 
 
 
diff --git a/flexx/app/modules.py b/flexx/app/modules.py
index e9d38b4..5d4436d 100644
--- a/flexx/app/modules.py
+++ b/flexx/app/modules.py
@@ -9,7 +9,7 @@ import json
 import time
 import types
 
-from ..pyscript import py2js, JSConstant, create_js_module, get_all_std_names
+from ..pyscript import py2js, RawJS, JSConstant, create_js_module, get_all_std_names
 
 from .model import Model
 from .asset import Asset, get_mod_name, module_is_package
@@ -108,7 +108,7 @@ class JSModule:
         # We use dicts so that we can "overwrite" them in interactive mode
         self._model_classes = {}
         self._pyscript_code = {}
-        self._json_values = {}
+        self._js_values = {}
         # Dependencies
         self._deps = {}  # mod_name -> [mod_as_name, *imports]
         # Caches
@@ -274,6 +274,14 @@ class JSModule:
                 # Import from another module
                 self._import(mod_name, val.__name__, name)
         
+        elif isinstance(val, RawJS):
+            # Verbatim JS
+            if val.__module__ == self.name:
+                self._provided_names.add(name)
+                self._js_values[name] = val.get_code()
+            else:
+                self._import(val.__module__, val.get_defined_name(name), name)
+        
         elif isinstance(val, json_types):
             # Looks like something we can serialize
             # Note: we have no way to determine where it is defined
@@ -284,7 +292,7 @@ class JSModule:
                 t = 'JS in "%s" uses %r but cannot serialize that value:\n%s'
                 raise ValueError(t % (self.filename, name, str(err)))
             self._provided_names.add(name)
-            self._json_values[name] = js
+            self._js_values[name] = js
         
         elif (getattr(val, '__module__', None) and
               getattr(sys.modules[val.__module__], '__pyscript__', False) and
@@ -334,8 +342,8 @@ class JSModule:
             # todo: collect stdlib funcs here
             # Insert serialized values
             value_lines = []
-            for key in sorted(self._json_values):
-                value_lines.append('var %s = %s;' % (key, self._json_values[key]))
+            for key in sorted(self._js_values):
+                value_lines.append('var %s = %s;' % (key, self._js_values[key]))
             js.insert(0, '')
             js.insert(0, '\n'.join(value_lines))
             # Prepare imports and exports
diff --git a/flexx/pyscript/__init__.py b/flexx/pyscript/__init__.py
index 76ec36f..7c759e7 100644
--- a/flexx/pyscript/__init__.py
+++ b/flexx/pyscript/__init__.py
@@ -261,7 +261,7 @@ class Parser(Parser3):
 from .functions import py2js, evaljs, evalpy, JSString
 from .functions import script2js, js_rename, create_js_module
 from .stdlib import get_full_std_lib, get_all_std_names
-from .stubs import JSConstant, window, undefined
+from .stubs import RawJS, JSConstant, window, undefined
 
 # Create stubs that mean something
 Infinity = float('inf')
diff --git a/flexx/pyscript/functions.py b/flexx/pyscript/functions.py
index d5004fd..028097c 100644
--- a/flexx/pyscript/functions.py
+++ b/flexx/pyscript/functions.py
@@ -75,9 +75,16 @@ def py2js(ob=None, new_name=None, **parser_options):
             if getattr(ob, '__name__', '') in ('', '<lambda>'):
                 raise ValueError('py2js() got anonymous function from '
                                  '"%s", line %i, %r.' % (filename, linenr, ob))
-            # Normalize indentation
+            # Normalize indentation, based on first line
             indent = len(lines[0]) - len(lines[0].lstrip())
-            lines = [line[indent:] for line in lines]
+            for i in range(len(lines)):
+                line = lines[i]
+                line_indent = len(line) - len(line.lstrip())
+                if line_indent < indent and line.strip():
+                    assert line.lstrip().startswith('#')  # only possible for comments
+                    lines[i] = indent * ' ' + line.lstrip()
+                else:
+                    lines[i] = line[indent:]
             # Skip any decorators
             while not lines[0].lstrip().startswith(thetype):
                 lines.pop(0)
diff --git a/flexx/pyscript/parser2.py b/flexx/pyscript/parser2.py
index 5afefc3..9c528a6 100644
--- a/flexx/pyscript/parser2.py
+++ b/flexx/pyscript/parser2.py
@@ -125,12 +125,12 @@ Defining functions
     def foo(x, *values):
         bar(x+1, *values)
     
-    # To write raw JS, define a function with only a docstring
+    # To write the function in raw JS, use the RawJS call
     def bar(a, b):
-        '''
+        RawJS('''
         var c = 4;
         return a + b + c;
-        '''
+        ''')
     
     # Lambda expressions
     foo = lambda x: x**2
@@ -214,9 +214,15 @@ Globals and nonlocal
 
 from . import commonast as ast
 from . import stdlib
+from . import logger
 from .parser1 import Parser1, JSError, unify, reprs  # noqa
 
 
+RAW_DOC_WARNING = ('Function %s only has a docstring, which used to be '
+                   'intepreted as raw JS. Wrap a call to RawJS(...) around the '
+                   'docstring, or add "pass" to the function body to prevent '
+                   'this behavior.')
+
 class Parser2(Parser1):
     """ Parser that adds control flow, functions, classes, and exceptions.
     """
@@ -768,7 +774,8 @@ class Parser2(Parser1):
         else:
             docstring = self.pop_docstring(node)
             if docstring and not node.body_nodes:
-                # Raw JS
+                # Raw JS - but deprecated
+                logger.warn(RAW_DOC_WARNING % node.name)
                 for line in docstring.splitlines():
                     code.append(self.lf(line))
             else:
diff --git a/flexx/pyscript/parser3.py b/flexx/pyscript/parser3.py
index 34e8403..62a6540 100644
--- a/flexx/pyscript/parser3.py
+++ b/flexx/pyscript/parser3.py
@@ -189,6 +189,7 @@ Aside from ``window``, ``flexx.pyscript`` also provides ``undefined``,
 from . import commonast as ast
 from . import stdlib
 from .parser2 import Parser2, JSError, unify  # noqa
+from .stubs import RawJS
 
 
 # This class has several `function_foo()` and `method_bar()` methods
@@ -216,6 +217,17 @@ class Parser3(Parser2):
             raise JSError('this_is_js() expects zero arguments.')
         return ('"this_is_js()"')
     
+    def function_RawJS(self, node):
+        if len(node.arg_nodes) == 1:
+            if not isinstance(node.arg_nodes[0], ast.Str):
+                raise JSError('RawJS needs a verbatim string (use multiple '
+                              'args to bypass PyScript\'s RawJS).')
+            lines = RawJS._str2lines(node.arg_nodes[0].value)
+            indent = (self._indent * 4) * ' '
+            return '\n'.join([indent + line for line in lines])
+        else:
+            return None  # maybe RawJS is a thing
+    
     ## Python buildin functions
     
     
diff --git a/flexx/pyscript/stubs.py b/flexx/pyscript/stubs.py
index d055e20..32ca3b8 100644
--- a/flexx/pyscript/stubs.py
+++ b/flexx/pyscript/stubs.py
@@ -5,6 +5,122 @@ Module that can dynamically generate stubs.
 import sys
 
 
+class RawJS:
+    """ An object to wrap verbatim code to be included in the generated
+    JavaScript. This serves a number of purposes:
+    
+    * Using code in PyScript that is not valid Python syntax, like regular
+      expressions or the jQuery object ``$``.
+    * Write high performance code that avoids Pythonic features like operator
+      overloading.
+    * In Flexx's module system it can be used to create a stub variable in
+      Python that *does* have a value in JS. This value can imported in other
+      modules, leading to a shared value also in JS.
+    
+    PyScript does not verify the syntax of the code, so write carefully!
+    To allow the features in the 3d point, this object has a magic touch:
+    the ``__module__`` attribute of an instance refers to the module in which it
+    was instantiated, and if it's a global, its defining name can be obtained.
+    
+    Example:
+    
+    .. code-block:: py
+        
+        # Syntax not usable in Py
+        myre = VerbatimJS('/ab+c/')
+        
+        # Code that should only execute on JS
+        foo = VerbatimJS('require("some.module")')
+        
+        # Performance
+        def bar(n):
+            res = []
+            VerbatimJS('''
+                for (var i=0; i<n; i++) {
+                    if (is_ok_num(i)) {
+                        res.push(i);
+                    }
+                }
+            ''')
+    
+    """
+    
+    def __init__(self, code, _resolve_defining_module=True):
+        if not isinstance(code, str):
+            raise TypeError('VerbatimJS requires str input.')
+        self._lines = self._str2lines(code)
+        
+        # Get the globals of the module in which this instance is defined, so
+        # that we can set __module__ and later obtain the name by which this 
+        # instance is known in that module. We use a trick here to get access
+        # to the stack frame while avoiding sys._getframe().
+        try:
+            raise Exception()
+        except Exception as err:
+            tb = getattr(err, '__traceback__', None)
+            if tb is None:  # Legacy Python 2.x
+                import sys
+                _, _, tb = sys.exc_info()
+            self._globals = tb.tb_frame.f_back.f_globals
+            del tb
+        self.__module__ = self._globals['__name__']
+        self._real_name = None
+    
+    def __repr__(self):
+        if len(self._lines) == 1 and len(self._lines[0]) < 60:
+            return '<%s "%s">' % (self.__class__.__name__, self.get_code(0))
+        else:
+            return '<%s with %i lines>' % (self.__class__.__name__, len(self._lines))
+    
+    def __str__(self):
+        return self.get_code(0)
+    
+    @classmethod
+    def _str2lines(cls, text):
+        """ Classmethod to split a text in lines, dedenting each line.
+        The first line's indentation will assume the minimal
+        indentation.
+        """
+        lines = text.replace('\r', '').split('\n')
+        lines[0] = lines[0].strip()  # firts line is always detented
+        if len(lines) > 1:
+            # Get minimal indentation
+            min_indent = 99999
+            for line in lines[1:]:
+                if line.strip():  # don't count empty lines
+                    min_indent = min(min_indent, len(line) - len(line.lstrip()))
+            # Remove indentation
+            for i in range(1, len(lines)):
+                lines[i] = lines[i][min_indent:].rstrip()
+        # Remove empty line only at beginning
+        if not lines[0]:
+            lines.pop(0)
+        return lines
+    
+    def get_defined_name(self, suggestion=None):
+        """ Get the name by which this object is known in the module in which
+        it is defined. Only works if it is a global. Returns '' otherwise.
+        If a suggestion is given and it is the correct name, this function
+        performs faster. The resulting name is cached internally.
+        """
+        if self._real_name is None:
+            self._real_name = ''  # could be defined not in the globals
+            if suggestion and self._globals.get(suggestion, None) is self:
+                self._real_name = suggestion
+            else:
+                for name, val in self._globals.items():
+                    if val is self:
+                        self._real_name = name
+                        break
+        return self._real_name
+    
+    def get_code(self, indent=0):
+        """ Get the code with the given indentation.
+        """
+        indent = indent * ' '
+        return '\n'.join([indent + line for line in self._lines])
+
+
 class JSConstant:
     """ Class to represent variables that are used in JS, and are considered
     global or otherwise available in a way that Python cannot know.
@@ -17,16 +133,19 @@ class JSConstant:
         return '<%s %s>' % (self.__class__.__name__, self._name)
 
 
-class Stub:
+class Stubs:
     
+    __name__ = __name__
+    __file__ = __file__
     JSConstant = JSConstant
+    RawJS = RawJS
     
     def __getattr__(self, name):
-        if name == 'JSConstant':
-            return self.JSConstant
+        if name in ('JSConstant', 'RawJS'):
+            return getattr(self, name)
         else:
             return self.JSConstant(name)
 
 
 # Seems hacky, but is supported: http://stackoverflow.com/a/7668273/2271927
-sys.modules[__name__] = Stub()
+sys.modules[__name__] = Stubs()
diff --git a/flexx/ui/examples/with_jquery.py b/flexx/ui/examples/with_jquery.py
index 1f90d0d..3a62c0b 100644
--- a/flexx/ui/examples/with_jquery.py
+++ b/flexx/ui/examples/with_jquery.py
@@ -6,6 +6,7 @@ frameworks.
 """
 
 from flexx import app, ui
+from flexx.pyscript import RawJS
 
 
 # Associate assets needed by this app.
@@ -22,11 +23,8 @@ class DatePicker(ui.Widget):
         def _init_phosphor_and_node(self):
             self.phosphor = self._create_phosphor_widget('input')
             self.node = self.phosphor.node
-            self._make_picker(self.node)
-        
-        def _make_picker(node):
-            """ $(node).datepicker(); // we cannot use $ as a variable name in PyScript
-            """
+            
+            RawJS('$')(self.node).datepicker()
 
 
 class Example(ui.Widget):
diff --git a/flexx/util/minify.py b/flexx/util/minify.py
index 79ddd2b..57003ee 100644
--- a/flexx/util/minify.py
+++ b/flexx/util/minify.py
@@ -38,7 +38,7 @@ def remove_comments(code):
             if not c:
                 break
             chars.append(c)
-            if c == c0 and chars[-1] != '\\':
+            if c == c0 and chars[-2] != '\\':
                 return
     def to_end_of_line():
         while True:
@@ -111,6 +111,7 @@ def tabbify(code):
     for line in code.splitlines():
         line2 = line.lstrip(' \t')
         indent_str = line[:len(line)-len(line2)]
-        indent_str = indent_str.replace('    ', '\t')
+        for s1, s2 in [('    ', '\t'), ('  ', '\t'), (' ', '')]:
+            indent_str = indent_str.replace(s1, s2)
         lines.append(indent_str + line2)
     return '\n'.join(lines)


Test patch:
diff --git a/flexx/app/tests/test_module.py b/flexx/app/tests/test_module.py
index 10572ca..33655dd 100644
--- a/flexx/app/tests/test_module.py
+++ b/flexx/app/tests/test_module.py
@@ -101,16 +101,19 @@ files['lib1'] = """
 
 files['lib2'] = """
     
+    from flexx.pyscript import RawJS
+    
     import sys
     sas = None
     
     offset = 3
+    bias = RawJS("[]")
     
     def cos(t):
-        return t + offset
+        return t + offset + bias
     
     def acos(t):
-        return t - offset
+        return t - offset + bias
     
     class AA(object):
         pass
@@ -119,11 +122,11 @@ files['lib2'] = """
 
 files['lib3'] = """
     from flxtest.lib1 import sin
-    from flxtest.lib2 import cos, offset
+    from flxtest.lib2 import cos, offset, bias
     from flxtest import x
     
     def tan(t):
-        return sin(t) / cos(t) + offset * 0  + x()
+        return sin(t) / cos(t) + offset * 0 + bias + x()
     
     def atan(t):
         return 1/tan(t)
@@ -214,6 +217,10 @@ def test_modules():
     assert 'offset = 3' in store['flxtest.lib2'].get_js()
     assert 'offset = 3' in store['flxtest.lib3'].get_js()
     
+    # But RawJS constants can be shared!
+    assert 'bias = []' in store['flxtest.lib2'].get_js()
+    assert 'bias = flxtest_lib2.bias' in store['flxtest.lib3'].get_js()
+    
     # So ,,, lib4 is omitted, right?
     assert 'flxtest.lib4' not in store
     assert 'magic_number' not in store['flxtest.foo'].get_js()
diff --git a/flexx/pyscript/tests/test_parser1.py b/flexx/pyscript/tests/test_parser1.py
index ffd93b8..8934b94 100644
--- a/flexx/pyscript/tests/test_parser1.py
+++ b/flexx/pyscript/tests/test_parser1.py
@@ -92,6 +92,15 @@ class TestExpressions:
         assert evalpy('a = [3, 4]; a *= 2; a') == '[ 3, 4, 3, 4 ]'
         assert evalpy('a = "ab"; a *= 2; a') == 'abab'
     
+    def test_raw_js_overloading(self):
+        # more RawJS tests in test_parser3.py
+        s1 = 'a=3; b=4; c=1; a + b - c'
+        s2 = 'a=3; b=4; c=1; RawJS("a + b") - c'
+        assert evalpy(s1) == '6'
+        assert evalpy(s2) == '6'
+        assert 'pyfunc' in py2js(s1)
+        assert 'pyfunc' not in py2js(s2)
+    
     def test_overload_funcs_dont_overload_real_funcs(self):
         assert evalpy('def add(a, b): return a-b\n\nadd(4, 1)') == '3'
         assert evalpy('def op_add(a, b): return a-b\n\nop_add(4, 1)') == '3'
diff --git a/flexx/pyscript/tests/test_parser2.py b/flexx/pyscript/tests/test_parser2.py
index 1d1f9db..fb0670e 100644
--- a/flexx/pyscript/tests/test_parser2.py
+++ b/flexx/pyscript/tests/test_parser2.py
@@ -2,7 +2,7 @@ import sys
 
 from flexx.util.testing import run_tests_if_main, raises, skipif
 
-from flexx.pyscript import JSError, py2js, evaljs, evalpy
+from flexx.pyscript import RawJS, JSError, py2js, evaljs, evalpy
 
 
 def nowhitespace(s):
@@ -465,10 +465,10 @@ class TestFunctions:
     def test_raw_js(self):
         
         def func(a, b):
-            """
+            RawJS("""
             var c = 3;
             return a + b + c;
-            """
+            """)
         
         code = py2js(func)
         assert evaljs(code + 'func(100, 10)') == '113'
diff --git a/flexx/pyscript/tests/test_parser3.py b/flexx/pyscript/tests/test_parser3.py
index b26d497..b166fdd 100644
--- a/flexx/pyscript/tests/test_parser3.py
+++ b/flexx/pyscript/tests/test_parser3.py
@@ -1,13 +1,41 @@
 import sys
 from flexx.util.testing import run_tests_if_main, raises, skip
 
-from flexx.pyscript import JSError, py2js, evaljs, evalpy
+from flexx.pyscript import JSError, py2js, evaljs, evalpy, RawJS
 
 
 def nowhitespace(s):
     return s.replace('\n', '').replace('\t', '').replace(' ', '')
 
 
+def foo(a, b):
+    x = RawJS('a + b')
+    y = 0
+    RawJS("""
+    for (i=0; i<8; i++) {
+        y += i * x;
+    }
+    """)
+    RawJS("""while (y>0) {
+        x += y;
+        y -= 1;
+    }
+    """)
+
+
+class TestSpecials:
+    
+    def test_rawJS(self):
+        
+        code = py2js(foo)
+        assert 'pyfunc' not in code
+        assert '    x =' in code
+        assert '    for' in code
+        assert '        y +=' in code
+        assert '    while' in code
+        assert '        y -=' in code
+
+
 class TestHardcoreBuildins:
     
     def test_isinstance(self):
@@ -163,6 +191,7 @@ class TestHardcoreBuildins:
         assert evalpy('list(range(2, 9, 2))') == '[ 2, 4, 6, 8 ]'
         assert evalpy('list(range(10, 3, -2))') == '[ 10, 8, 6, 4 ]'
 
+
 class TestOtherBuildins:
     
     # def test_allow_overload(self):
diff --git a/flexx/pyscript/tests/test_stubs.py b/flexx/pyscript/tests/test_stubs.py
new file mode 100644
index 0000000..a540d1b
--- /dev/null
+++ b/flexx/pyscript/tests/test_stubs.py
@@ -0,0 +1,82 @@
+from flexx.util.testing import run_tests_if_main, raises
+
+from flexx import pyscript
+from flexx.pyscript import RawJS
+
+
+def test_stubs():
+    from flexx.pyscript.stubs import window, undefined, omgnotaname
+    
+    assert isinstance(window, pyscript.JSConstant)
+    assert isinstance(undefined, pyscript.JSConstant)
+    assert isinstance(omgnotaname, pyscript.JSConstant)
+
+
+def test_raw_js():
+    
+    with raises(TypeError):
+        RawJS()
+    with raises(TypeError):
+        RawJS(3)
+    
+    # Empty
+    r1 = RawJS('')
+    assert str(r1) == ''
+    assert r1.get_code() == ''
+    assert r1.get_code(4) == ''
+    assert '0' in repr(r1)
+    assert r1.__module__.endswith(__name__)
+    
+    # Short single line
+    r2 = RawJS('require("foobar")')
+    assert 'require(' in repr(r2)
+    assert 'require(' in str(r2)
+    assert r2.get_code().startswith('require')
+    assert r2.get_code(4).startswith('    require')
+    assert r2.get_code(2).startswith('  require')
+    assert '\n' not in r2.get_code()
+    
+    # Long single line
+    r2b = RawJS('require("foobar")'*10)
+    assert 'require(' not in repr(r2b)
+    assert '1' in repr(r2b)
+    
+    # Multiline, start at first line
+    r3 = RawJS("""for ... {
+                      yyyy  
+                  }
+               """)
+    assert 'lines' in repr(r3)
+    assert 'for ...' in str(r3)
+    assert str(r3).endswith('}\n')
+    assert r3.get_code().count('\n') == 3
+    assert r3.get_code().startswith('for')
+    assert r3.get_code(4).startswith('    for')
+    assert '\n    yyyy\n' in r3.get_code(0)
+    assert '\n        yyyy\n' in r3.get_code(4)
+    
+    # Multiline, exactly the same, but start at second line; same results
+    r4 = RawJS("""
+        for ... {
+            yyyy  
+        }
+        """)
+    assert 'lines' in repr(r4)
+    assert 'for ...' in str(r4)
+    assert str(r4).endswith('}\n')
+    assert r4.get_code().count('\n') == 3
+    assert r4.get_code().startswith('for')
+    assert r4.get_code(4).startswith('    for')
+    assert '\n    yyyy\n' in r4.get_code(0)
+    assert '\n        yyyy\n' in r4.get_code(4)
+    
+    # Multiline, now newline at the ned
+    r5 = RawJS("""
+        for ... {
+            yyyy  
+        }""")
+    assert r5.get_code().count('\n') == 2
+    assert str(r5).endswith('}')
+
+
+run_tests_if_main()