Repository: flexxui/pscript

Issue:
parse error
I have the following dictionary. pscript fails on ñ, 'cause if i replace it with `\xf1` it compiles.
```
langs = {'de': ['De', "Deutsch"], 'en': ['En', "English"],
         'es': ['Es', "Español"], 'fr': ['Fr', "Français"],
         'ru': ['Ru', "Русский"]}
```

Exception:
`RuntimeError: Cannot do formatted string literals yet: 'es': ['Es', "Español"], 'fr': ['Fr', "Français"],`
pscript thinks that `fr` is a prefix of a string.

Discussion:

Patch:
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..6bd7633
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,5 @@
+include LICENSE README.md
+
+global-exclude .git*
+global-exclude *.pyo
+global-exclude *.pyc
diff --git a/pscript/__init__.py b/pscript/__init__.py
index 5fb654a..ec73544 100644
--- a/pscript/__init__.py
+++ b/pscript/__init__.py
@@ -186,7 +186,7 @@ Supported Python conveniences:
   round, int, float, str, bool, abs, divmod, all, any, enumerate, zip,
   reversed, sorted, filter, map.
 * all methods of list, dict and str are supported (except a few string
-  methods: encode, format, format_map, isdecimal, isdigit, isprintable, maketrans)
+  methods: encode, format_map, isprintable, maketrans).
 * the default return value of a function is ``None``/``null`` instead
   of ``undefined``.
 * list concatenation using the plus operator, and list/str repeating
diff --git a/pscript/commonast.py b/pscript/commonast.py
index 6210183..dcdaf7a 100644
--- a/pscript/commonast.py
+++ b/pscript/commonast.py
@@ -223,6 +223,30 @@ class Str(Node):
     """
     __slots__ = 'value',
 
+class FormattedValue(Node):
+    """ Node representing a single formatting field in an f-string. If the
+    string contains a single formatting field and nothing else the node can be
+    isolated, otherwise it appears in JoinedStr.
+    
+    Attributes:
+        value_node: an expression (can be anything).
+        conversion: a string, '' means no formatting, 's' means !s string
+            formatting, 'r' means !r repr formatting, 'a' means !a ascii
+            formatting.
+        format_node:  a JoinedStr node reprensenting the formatting, or None
+            if no format was specified. Both conversion and format_node can be
+            set at the same time.
+    """
+    __slots__ = 'value_node', 'conversion', 'format_node'
+
+class JoinedStr(Node):
+    """ An f-string, comprising a series of FormattedValue and Str nodes.
+    
+    Attributes:
+        value_nodes: list of Str and FormattedValue nodes.
+    """
+    __slots__ = 'value_nodes',
+
 class Bytes(Node):
     """
     Attributes:
@@ -790,21 +814,33 @@ class NativeAstConverter:
         return Num(n.n)
     
     def _convert_Str(self, n):
-        # Get string modifier char
-        line = self._lines[n.lineno-1]
-        pre = ''
-        if line[n.col_offset] not in '"\'':
-            pre += line[n.col_offset]
-            if line[n.col_offset + 1] not in '"\'':
-                pre += line[n.col_offset + 1]
-        # Formatted, bytes?
-        if 'f' in pre:
-            raise RuntimeError('Cannot do formatted string literals yet: ' +
-                               line)
-        if pyversion < (3, ) and 'b' in pre:
-            return Bytes(n.s)
+        # We check the string prefix here. We only really need it in Python 2,
+        # because u is not needed in py3, and b and r are resolved by the lexer,
+        # and f as well (resulting in  JoinedStr or  FormattedValue).
+        # Note that the col_offset of the node seems 1 off when the string is
+        # a key in a dict :/ (PScript issue #15)
+        if pyversion < (3, ):
+            line = self._lines[n.lineno-1]
+            i = n.col_offset
+            i = i - 1 if (i > 0 and line[i-1] in 'rufb"\'') else i
+            pre = ''
+            if line[i] not in '"\'':
+                pre += line[i]
+                if line[i + 1] not in '"\'':
+                    pre += line[i + 1]
+            if 'b' in pre:
+                return Bytes(n.s)
         return Str(n.s)
     
+    def _convert_JoinedStr(self, n):
+        c = self._convert
+        return JoinedStr([c(x) for x in n.values])
+    
+    def _convert_FormattedValue(self, n):
+        conversion = '' if n.conversion < 0 else chr(n.conversion)
+        return FormattedValue(self._convert(n.value), conversion,
+                              self._convert(n.format_spec))
+    
     def _convert_Bytes(self, n):
         return Bytes(n.s)
     
diff --git a/pscript/parser0.py b/pscript/parser0.py
index be23d70..f7afe46 100644
--- a/pscript/parser0.py
+++ b/pscript/parser0.py
@@ -299,7 +299,7 @@ class Parser0:
         lineno = getattr(node, 'lineno', -1)
         if self._pysource:
             filename, lineno = self._pysource
-            lineno += node.lineno - 1
+            lineno += node.lineno
         
         msg = 'Error processing %s-node' % (node.__class__.__name__)
         if classNode:
diff --git a/pscript/parser1.py b/pscript/parser1.py
index 16dbab2..44bd08b 100644
--- a/pscript/parser1.py
+++ b/pscript/parser1.py
@@ -47,12 +47,39 @@ Slicing and subscriping
 String formatting
 -----------------
 
-Basic string formatting is supported for "%s", "%f", and "%i".
+String formatting is supported in various forms.
 
 .. pscript_example::
     
-    "value: %f" % val
-    "%s: %f" % (name, val)
+    # Old school
+    "value: %g" % val
+    "%s: %0.2f" % (name, val)
+    
+    # Modern
+    "value: {:g}".format(val)
+    "{}: {:3.2f}".format(name, val)
+    
+    # F-strings (python 3.6+)
+    f"value: {val:g}"
+    f"{name}: {val:3.2f}"
+    
+    # This also works
+    t = "value: {:g}"
+    t.format(val)
+    
+    # But this does not (because PScript cannot know whether t is str or float)
+    t = "value: %g"
+    t % val
+
+Kinds of formatting that is supported:
+
+* Float, exponential en "general" number formatting.
+* Specifying precision for numbers.
+* Padding of number with "+" or " ".
+* Repr-formatting.
+
+At the moment, PScript does not support advanced features such as string
+padding.
 
 
 Assignments
@@ -168,11 +195,31 @@ from .parser0 import Parser0, JSError, unify, reprs  # noqa
 # Define builtin stuff for which we know that it returns a bool or int
 _bool_funcs = 'hasattr', 'all', 'any', 'op_contains', 'op_equals', 'truthy'
 _bool_meths = ('count', 'isalnum', 'isalpha', 'isidentifier', 'islower',
-               'isnumeric', 'isspace', 'istitle', 'isupper', 'startswith')
+               'isnumeric', 'isdigit', 'isdecimal', 'isspace', 'istitle',
+               'isupper', 'startswith')
 returning_bool = tuple([stdlib.FUNCTION_PREFIX + x + '(' for x in _bool_funcs] +
                        [stdlib.METHOD_PREFIX + x + '.' for x in _bool_meths])
 
 
+# precompile regexp to help determine whether a string is an identifier
+isidentifier1 = re.compile(r'^\w+$', re.UNICODE)
+
+reserved_names = (
+    'abstract', 'instanceof', 'boolean', 'enum', 'switch', 'export',
+    'interface', 'synchronized', 'extends', 'let', 'case', 'throw',
+    'catch', 'final', 'native', 'throws', 'new', 'transient',
+    'const', 'package', 'function', 'private', 'typeof', 'debugger', 'goto',
+    'protected', 'var', 'default', 'public', 'void', 'delete', 'implements',
+    'volatile', 'do', 'static',
+    # Commented, because are disallowed in Python too.
+    # 'else', 'break', 'finally', 'class', 'for', 'try', 'continue', 'if',
+    # 'return', 'import', 'while', 'in', 'with',
+    # Commented for pragmatic reasons
+    # 'super', 'float', 'this', 'int', 'byte', 'long', 'char', 'short',
+    # 'double', 'null', 'true', 'false',
+    )
+
+
 class Parser1(Parser0):
     """ Parser that add basic functionality like assignments,
     operations, function calls, and indexing.
@@ -186,6 +233,37 @@ class Parser1(Parser0):
     def parse_Str(self, node):
         return reprs(node.value)
     
+    def parse_JoinedStr(self, node):
+        parts, value_nodes = [], []
+        for n in node.value_nodes:
+            if isinstance(n, ast.Str):
+                parts.append(n.value)
+            else:
+                assert isinstance(n, ast.FormattedValue)
+                parts.append('{' + self._parse_FormattedValue_fmt(n) + '}')
+                value_nodes.append(n.value_node)
+        thestring = reprs(''.join(parts))
+        return self.use_std_method(thestring, 'format', value_nodes)
+    
+    def parse_FormattedValue(self, node):  # can als be present standalone
+        thestring = "{" + self._parse_FormattedValue_fmt(node) + "}"
+        return self.use_std_method(thestring, 'format', [node.value_node])
+    
+    def _parse_FormattedValue_fmt(self, node):
+        """ Return fmt for a FormattedValue node.
+        """
+        fmt = ''
+        if node.conversion:
+            fmt += '!' + node.conversion
+        if node.format_node and len(node.format_node.value_nodes) > 0:
+            if len(node.format_node.value_nodes) > 1:
+                raise JSError('String formatting only supports singleton format spec.')
+            spec_node = node.format_node.value_nodes[0]
+            if not isinstance(spec_node, ast.Str):
+                raise JSError('String formatting only supports string format spec.')
+            fmt += ':' + spec_node.value
+        return fmt
+    
     def parse_Bytes(self, node):
         raise JSError('No Bytes in JS')
     
@@ -209,25 +287,33 @@ class Parser1(Parser0):
     def parse_Dict(self, node):
         # Oh JS; without the outer braces, it would only be an Object if used
         # in an assignment ...
+        use_make_dict_func = False
         code = ['({']
         for key, val in zip(node.key_nodes, node.value_nodes):
             if isinstance(key, (ast.Num, ast.NameConstant)):
                 code += self.parse(key)
-            elif isinstance(key, ast.Str):
-                if ' ' in key.value:
-                    raise JSError('Keys in a literal dict cannot contain spaces.')
+            elif (isinstance(key, ast.Str) and isidentifier1.match(key.value) and
+                                               key.value[0] not in '0123456789'):
                 code += key.value
             else:
-                # code += ['['] + self.parse(key) + [']']  # this actually breaks on IE
-                raise JSError('Computed dict attributes are not supported on IE :/')
+                use_make_dict_func = True
+                break
             code.append(': ')
             code += self.parse(val)
             code.append(', ')
         if node.key_nodes:
             code.pop(-1)  # skip last comma
         code.append('})')
-        return code
         
+        # Do we need to use the fallback?
+        if use_make_dict_func:
+            func_args = []
+            for key, val in zip(node.key_nodes, node.value_nodes):
+                func_args += [unify(self.parse(key)), unify(self.parse(val))]
+            self.use_std_function('create_dict', [])
+            return stdlib.FUNCTION_PREFIX + 'create_dict(' + ', '.join(func_args) + ')'
+        return code
+    
     def parse_Set(self, node):
         raise JSError('No Set in JS')
     
@@ -246,6 +332,8 @@ class Parser1(Parser0):
     def parse_Name(self, node, fullname=None):
         # node.ctx can be Load, Store, Del -> can be of use somewhere?
         name = node.name
+        if name in reserved_names:
+            raise JSError('Cannot use reserved name %s as a variable name!' % name)
         if self.vars.is_known(name):
             return self.with_prefix(name)
         if self._scope_prefix:
@@ -310,36 +398,41 @@ class Parser1(Parser0):
         return [left, op, right]
     
     def _format_string(self, node):
-        # Get left end, stripped from the separator
+        # Get value_nodes
+        if isinstance(node.right_node, (ast.Tuple, ast.List)):
+            value_nodes = node.right_node.element_nodes
+        else:
+            value_nodes = [node.right_node]
+        
+        # Is the left side a string? If not, exit early
+        # This works, but we cannot know whether the left was a string or number :P
+        # if not isinstance(node.left_node, ast.Str):
+        #     thestring = unify(self.parse(node.left_node))
+        #     thestring += ".replace(/%([0-9\.\+\-\#]*[srdeEfgGioxXc])/g, '{:$1}')"
+        #     return self.use_std_method(thestring, 'format', value_nodes) 
+        
+        assert isinstance(node.left_node, ast.Str)
         left = ''.join(self.parse(node.left_node))
         sep, left = left[0], left[1:-1]
-        # Get items
-        right = node.right_node
-        if isinstance(right, (ast.Tuple, ast.List)):
-            items = [unify(self.parse(n)) for n in right.element_nodes]
-        else:
-            items = [unify(self.parse(right))]
+        
         # Get matches
         matches = list(re.finditer(r'%[0-9\.\+\-\#]*[srdeEfgGioxXc]', left))
-        if len(matches) != len(items):
+        if len(matches) != len(value_nodes):
             raise JSError('In string formatting, number of placeholders '
                             'does not match number of replacements')
         # Format
-        code = []
+        parts = []
         start = 0
-        for i, m in enumerate(matches):
+        for m in matches:
             fmt = m.group(0)
-            if fmt in ('%s', '%f', '%i', '%d', '%g'):
-                code.append(sep + left[start:m.start()] + sep)
-                code.append(' + ' + items[i] + ' + ')
-            elif fmt == '%r':
-                code.append(sep + left[start:m.start()] + sep)
-                code.append(' + %s + ' % self.use_std_function('repr', [items[i]]))
-            else:
-                raise JSError('Unsupported string formatting %r' % fmt)
+            fmt = {'%r': '!r', '%s': ''}.get(fmt, ':' + fmt[1:])
+            # Add the part in front of the match (and after prev match)
+            parts.append(left[start:m.start()])
+            parts.append("{%s}" % fmt)
             start = m.end()
-        code.append(sep + left[start:] + sep)
-        return code
+        parts.append(left[start:])
+        thestring = sep + ''.join(parts) + sep
+        return self.use_std_method(thestring, 'format', value_nodes)
     
     def _wrap_truthy(self, node):
         """ Wraps an operation in a truthy call, unless its not necessary. """
diff --git a/pscript/parser2.py b/pscript/parser2.py
index 97257ec..93b1ef5 100644
--- a/pscript/parser2.py
+++ b/pscript/parser2.py
@@ -446,9 +446,11 @@ class Parser2(Parser1):
         if (True and isinstance(node.test_node, ast.Call) and
                      isinstance(node.test_node.func_node, ast.Name) and
                      node.test_node.func_node.name == 'this_is_js'):
-            code = [self.lf('{ /* if this_is_js() */')]
+            code = [self.lf('if ('), 'true', ') ', '{ /* if this_is_js() */']
+            self._indent += 1
             for stmt in node.body_nodes:
                 code += self.parse(stmt)
+            self._indent -= 1
             code.append(self.lf('}'))
             return code
         
@@ -499,6 +501,7 @@ class Parser2(Parser1):
             elif isinstance(f, ast.Name) and f.name in ('xrange', 'range'):
                 sure_is_range = [''.join(self.parse(arg)) for arg in 
                                  node.iter_node.arg_nodes]
+                iter = 'range'  # stub to prevent the parsing of iter_node below
         
         # Otherwise we parse the iter
         if iter is None:
@@ -557,7 +560,7 @@ class Parser2(Parser1):
                 start, end, step = nums[0], nums[1], nums[2]
             # Build for-loop in JS
             t = 'for ({i} = {start}; {i} < {end}; {i} += {step})'
-            if step.lstrip('+-').isnumeric() and float(step) < 0:
+            if step.lstrip('+-').isdecimal() and float(step) < 0:
                 t = t.replace('<', '>')
             assert len(target) == 1
             t = t.format(i=target[0], start=start, end=end, step=step) + ' {'
diff --git a/pscript/parser3.py b/pscript/parser3.py
index 0c34e78..f250879 100644
--- a/pscript/parser3.py
+++ b/pscript/parser3.py
@@ -10,8 +10,7 @@ tuple, range, pow, sum, round, int, float, str, bool, abs, divmod, all,
 any, enumerate, zip, reversed, sorted, filter, map.
 
 Further all methods for list, dict and str are implemented (except str
-methods: encode, decode, format, format_map, isdecimal, isdigit,
-isprintable, maketrans).
+methods: encode, decode, format_map, isprintable, maketrans).
 
 .. pscript_example::
 
@@ -415,6 +414,11 @@ class Parser3(Parser2):
                 else:
                     raise JSError('Invalid keyword argument for sort: %r' % kw.name)
             return self.use_std_method(base, 'sort', [key, reverse])
+    
+    def method_format(self, node, base):
+        if node.kwarg_nodes:
+            raise JSError('Method format() does not support keyword args.')
+        return self.use_std_method(base, 'format', node.arg_nodes)
 
 
 # Add functions and methods to the class, using the stdib functions ...
diff --git a/pscript/stdlib.py b/pscript/stdlib.py
index 0869ce8..df897ea 100644
--- a/pscript/stdlib.py
+++ b/pscript/stdlib.py
@@ -141,6 +141,12 @@ FUNCTIONS['op_instantiate'] = """function (ob, args) { // nargs: 2
     }
 }"""
 
+FUNCTIONS['create_dict'] = """function () {
+    var d = {};
+    for (var i=0; i<arguments.length; i+=2) { d[arguments[i]] = arguments[i+1]; }
+    return d;
+}"""
+
 FUNCTIONS['merge_dicts'] = """function () {
     var res = {};
     for (var i=0; i<arguments.length; i++) {
@@ -219,7 +225,7 @@ FUNCTIONS['list'] = """function (x) {
 }"""
 
 FUNCTIONS['range'] = """function (start, end, step) {
-var i, res = [];
+    var i, res = [];
     var val = start;
     var n = (end - start) / step;
     for (i=0; i<n; i++) {
@@ -229,6 +235,59 @@ var i, res = [];
     return res;
 }"""
 
+FUNCTIONS['format'] = """function (v, fmt) {  // nargs: 2
+    fmt = fmt.toLowerCase();
+    var s = String(v);
+    if (fmt.indexOf('!r') >= 0) {
+        try { s = JSON.stringify(v); } catch (e) { s = undefined; }
+        if (typeof s === 'undefined') { s = v._IS_COMPONENT ? v.id : String(v); }
+    }
+    var i0 = fmt.indexOf(':');
+    if (i0 < 0) {
+    } else if (fmt.indexOf('i', i0) > i0) { // integer formatting
+        s = Number.parseInt(v).toFixed(0);
+    } else if (fmt.indexOf('f', i0) > i0) {  // float formatting
+        v = Number.parseFloat(v);
+        var spec = fmt.slice(i0+1, fmt.indexOf('f', i0));
+        var decimals = 6;
+        if (spec.indexOf('.') >= 0) {
+            var decimals = Number(spec.slice(spec.indexOf('.')+1));
+        }
+        s = v.toFixed(decimals);
+    } else if (fmt.indexOf('e', i0) > i0) {  // exp formatting
+        v = Number.parseFloat(v);
+        var precision = 6;
+        var spec = fmt.slice(i0+1, fmt.indexOf('e', i0));
+        if (spec.indexOf('.') >= 0) {
+            precision = Number(spec.slice(spec.indexOf('.')+1)) || 1;
+        }
+        s = v.toExponential(precision);
+    } else if (fmt.indexOf('g', i0) > i0) {  // "general" formatting
+        v = Number.parseFloat(v);
+        var precision = 6;
+        var spec = fmt.slice(i0+1, fmt.indexOf('g', i0));
+        if (spec.indexOf('.') >= 0) {
+            precision = Number(spec.slice(spec.indexOf('.')+1)) || 1;
+        }
+        // Exp or decimal?
+        s = v.toExponential(precision-1);
+        var s1 = s.slice(0, s.indexOf('e')), s2 = s.slice(s.indexOf('e'));
+        if (s2.length == 3) { s2 = 'e' + s2[1] + '0' + s2[2]; }
+        var exp = Number(s2.slice(1));
+        if (exp >= -4 && exp < precision) { s1=v.toPrecision(precision); s2=''; }
+        // Skip trailing zeros and dot
+        var j = s1.length-1;
+        while (j>0 && s1[j] == '0') { j-=1; }
+        s1 = s1.slice(0, j+1);
+        if (s1.endsWith('.')) { s1 = s1.slice(0, s1.length-1); }
+        s = s1 + s2;
+    }
+    if (i0 >= 0 && v > 0) {
+        if (fmt[i0+1] == '+') { s = '+' + s; }
+        if (fmt[i0+1] == ' ') { s = ' ' + s; }
+    }
+    return s;
+}"""
 
 ## Normal functions
 
@@ -558,8 +617,7 @@ METHODS['values'] = """function () { // nargs: 0
 
 ## String only
 
-# ignores: encode, decode, format, format_map, isdecimal, isdigit,
-# isprintable, maketrans
+# ignores: encode, decode, format_map, isprintable, maketrans
 
 # Not a Python method, but a method that we need, and is only ECMA 6
 # http://stackoverflow.com/a/5450113/2271927
@@ -615,6 +673,31 @@ METHODS['find'] = """function (x, start, stop) { // nargs: 1 2 3
     return -1;
 }"""
 
+METHODS['format'] = """function () {
+    if (this.constructor !== String) return this.KEY.apply(this, arguments);
+    var parts = [], i = 0, i1, i2;
+    var itemnr = -1;
+    while (i < this.length) {
+        // find opening
+        i1 = this.indexOf('{', i);
+        if (i1 < 0 || i1 == this.length-1) { break; }
+        if (this[i1+1] == '{') {parts.push(this.slice(i, i1+1)); i = i1 + 2; continue;}
+        // find closing
+        i2 = this.indexOf('}', i1);
+        if (i2 < 0) { break; }
+        // parse
+        itemnr += 1;
+        var fmt = this.slice(i1+1, i2);
+        var index = fmt.split(':')[0].split('!')[0];
+        index = index? Number(index) : itemnr
+        var s = FUNCTION_PREFIXformat(arguments[index], fmt);
+        parts.push(this.slice(i, i1), s);
+        i = i2 + 1;
+    }
+    parts.push(this.slice(i));
+    return parts.join('');
+}"""
+
 METHODS['isalnum'] = """function () { // nargs: 0
     if (this.constructor !== String) return this.KEY.apply(this, arguments);
     return Boolean(/^[A-Za-z0-9]+$/.test(this));
@@ -625,13 +708,6 @@ METHODS['isalpha'] = """function () { // nargs: 0
     return Boolean(/^[A-Za-z]+$/.test(this));
 }"""
 
-# METHODS['isdecimal'] = """function () {
-#     if (this.constructor !== String) return this.KEY.apply(this, arguments);
-#     return Boolean(/^[0-9]+$/.test(this));
-# }"""
-# 
-# METHODS['isdigit'] = METHODS['isdecimal']
-
 METHODS['isidentifier'] = """function () { // nargs: 0
     if (this.constructor !== String) return this.KEY.apply(this, arguments);
     return Boolean(/^[A-Za-z_][A-Za-z0-9_]*$/.test(this));
@@ -643,11 +719,25 @@ METHODS['islower'] = """function () { // nargs: 0
     return low != high && low == this;
 }"""
 
-METHODS['isnumeric'] = """function () { // nargs: 0
+METHODS['isdecimal'] = """function () { // nargs: 0
     if (this.constructor !== String) return this.KEY.apply(this, arguments);
     return Boolean(/^[0-9]+$/.test(this));
 }"""
 
+# The thing about isdecimal, isdigit and isnumeric.
+# https://stackoverflow.com/a/36800319/2271927
+#
+# * isdecimal() (Only Decimal Numbers)
+# * str.isdigit() (Decimals, Subscripts, Superscripts)
+# * isnumeric() (Digits, Vulgar Fractions, Subscripts, Superscripts,
+#   Roman Numerals, Currency Numerators)
+#
+# In other words, isdecimal is the most strict. We used to have
+# isnumeric with isdecimal's implementation, so we provide isnumeric
+# and isdigit as aliases for now.
+
+METHODS['isnumeric'] = METHODS['isdigit'] = METHODS['isdecimal']
+
 METHODS['isspace'] = """function () { // nargs: 0
     if (this.constructor !== String) return this.KEY.apply(this, arguments);
     return Boolean(/^\\s+$/.test(this));


Test patch:
diff --git a/pscript/tests/test_parser1.py b/pscript/tests/test_parser1.py
index d9300a7..863abbf 100644
--- a/pscript/tests/test_parser1.py
+++ b/pscript/tests/test_parser1.py
@@ -1,5 +1,7 @@
 from pscript.testing import run_tests_if_main, raises
 
+import sys
+
 import pscript
 from pscript import JSError, py2js, evaljs, evalpy, Parser
 
@@ -68,16 +70,66 @@ class TestExpressions:
         assert evalpy('True or False') == 'true'
         # Bug
         assert evalpy('(9-3-3)/3') == '1'
-        
-        # string formatting
+    
+    def test_string_formatting1(self):
+        # string formatting that we already had
         assert evalpy('"%s" % "bar"') == 'bar'
         assert evalpy('"-%s-" % "bar"') == '-bar-'
         assert evalpy('"foo %s foo" % "bar"') == 'foo bar foo'
         assert evalpy('"x %i" % 6') == 'x 6'
-        assert evalpy('"x %f" % 6') == 'x 6'
-        assert evalpy('"%s: %f" % ("value", 6)') == 'value: 6'
+        assert evalpy('"x %g" % 6') == 'x 6'
+        assert evalpy('"%s: %f" % ("value", 6)') == 'value: 6.000000'
         assert evalpy('"%r: %r" % ("value", 6)') == '"value": 6'
     
+    def test_string_formatting2(self):
+        
+        py2jslight = lambda x: py2js(x, inline_stdlib=False)
+        
+        # Verify that percent-formatting produces same JS as str.format
+        assert py2jslight("'hi %i' % a") == py2jslight("'hi {:i}'.format(a)")
+        assert py2jslight("'hi %i %+i' % (a, b)") == py2jslight("'hi {:i} {:+i}'.format(a, b)")
+        assert py2jslight("'hi %f %1.2f' % (a, b)") == py2jslight("'hi {:f} {:1.2f}'.format(a, b)")
+        assert py2jslight("'hi %s %r' % (a, b)") == py2jslight("'hi {} {!r}'.format(a, b)")
+        
+        if sys.version_info < (3, 6):
+            return
+        
+        # Verify that f-string formatting produces same JS as str.format - Python 3.6+
+        assert py2jslight("f'hi {a:i}'") == py2jslight("'hi {:i}'.format(a)")
+        assert py2js("f'hi {a:i} {b:+i}'") == py2js("'hi {:i} {:+i}'.format(a, b)")
+        assert py2jslight("f'hi {a:f} {b:1.2f}'") == py2jslight("'hi {:f} {:1.2f}'.format(a, b)")
+        assert py2jslight("f'hi {a} {b!r}'") == py2jslight("'hi {} {!r}'.format(a, b)")
+    
+    def test_string_formatting3(self):
+        # Verify fancy formatting (mosly for numbers)
+        # We don't support every kind of fortting that Python does.
+        
+        x = 'a = 3.1415926535; b = 7; c = "foo"; d = 314159265.35; e = 0.0031415926535;'
+        # i formatting
+        assert evalpy(x + "'hi {:i}'.format(b)") == 'hi 7'
+        # f formatting
+        assert evalpy(x + "'hi {:i} {:+i} {: i}'.format(b, b, b)") == 'hi 7 +7  7'
+        assert evalpy(x + "'hi {:f} {:1.0f} {:1.2f}'.format(a, a, a)") == 'hi 3.141593 3 3.14'
+        # g formatting, these outputs are (manually) validated with Python
+        assert evalpy(x + "'hi {:g} {:.1g} {:.3g}'.format(a, a, a)") == 'hi 3.14159 3 3.14'
+        assert evalpy(x + "'hi {:g} {:.1g} {:.3g}'.format(d, d, d)") == 'hi 3.14159e+08 3e+08 3.14e+08'
+        assert evalpy(x + "'hi {:g} {:.1g} {:.3g}'.format(e, e, e)") == 'hi 0.00314159 0.003 0.00314'
+        # String and repr formatting
+        assert evalpy(x + "'hi {} {!s} {!r}'.format(c, c, c)") == 'hi foo foo "foo"'
+    
+    def test_string_formatting4(self):
+        
+        x = 'a = 3; b = 4; '
+        
+        # Setting positions in format string
+        assert evalpy(x + "'hi {1:g} {1:+g} {0}'.format(a, b)") == 'hi 4 +4 3'
+        
+        # Using a predefined template string for .format()
+        assert evalpy(x + "t = 'hi {} {}'; t.format(a, b)") == 'hi 3 4'
+        
+        # Using a predefined template string for % - we cannot do this, unfortunately!
+        # assert evalpy(x + "t = 'hi %i %i'; t % (a, b)") == 'hi 3 4'
+    
     def test_overloaded_list_ops(self):
         assert evalpy('[1, 2] + [3, 4]') == '[ 1, 2, 3, 4 ]'
         assert evalpy('[3, 4] + [1, 2]') == '[ 3, 4, 1, 2 ]'
@@ -311,8 +363,18 @@ class TestExpressions:
         
         assert py2js('{"foo": 3, "bar": 4}') == '({foo: 3, bar: 4});'
         assert evalpy('a={"foo": 3, "bar": 4};a') == '{ foo: 3, bar: 4 }'
-        with raises(JSError):
-            assert evalpy('bla="foo";a={bla: 3, bar: 4};a') == '{ foo: 3, bar: 4 }'
+    
+    def test_dict_literals(self):
+        # JS has a different way to define dict literals, with limitation
+        # (especially on IE), so we add some magic sause to make it work.
+        
+        def tester1():
+            a = 'foo'
+            d = {a: 'bar1', 2: 'bar2', 'sp' + 'am': 'bar3'}
+            print(d.foo, d[2], d.spam)
+        
+        js = py2js(tester1)
+        assert evaljs(js + 'tester1()') == 'bar1 bar2 bar3\nnull'
     
     def test_ignore_import_of_compiler(self):
         modname = pscript.__name__
diff --git a/pscript/tests/test_parser3.py b/pscript/tests/test_parser3.py
index ee39d85..3dd0ae5 100644
--- a/pscript/tests/test_parser3.py
+++ b/pscript/tests/test_parser3.py
@@ -607,6 +607,20 @@ class TestStrMethods:
         assert evalpy('"0a1b2c".isalpha()') == 'false'
         assert evalpy('"0a_".isalpha()') == 'false'
     
+    def test_isdecimal(self):
+        assert evalpy('"".isdecimal()') == 'false'
+        assert evalpy('"012".isdecimal()') == 'true'
+        assert evalpy('"abc".isdecimal()') == 'false'
+        assert evalpy('"0a1b2c".isdecimal()') == 'false'
+        assert evalpy('"0a_".isdecimal()') == 'false'
+    
+    def test_isdigit(self):
+        assert evalpy('"".isdigit()') == 'false'
+        assert evalpy('"012".isdigit()') == 'true'
+        assert evalpy('"abc".isdigit()') == 'false'
+        assert evalpy('"0a1b2c".isdigit()') == 'false'
+        assert evalpy('"0a_".isdigit()') == 'false'
+    
     def test_isnumeric(self):
         assert evalpy('"".isnumeric()') == 'false'
         assert evalpy('"012".isnumeric()') == 'true'
@@ -780,10 +794,15 @@ class TestStrMethods:
         code = "table = {'a':'x', 'b':'y', 'c': None}\n"
         assert evalpy(code + "'abcde'.translate(table)") == "xyde"
     
+    def test_format(self):
+        # Covered more extensively in test_parser1(), but we need the method
+        # to pass the test below ...
+        assert evalpy("'{:+0.2f}'.format(1.23456)") == "+1.23"
+    
     def test_that_all_str_methods_are_tested(self):
         tested = set([x.split('_')[1] for x in dir(self) if x.startswith('test_')])
         needed = set([x for x in dir(str) if not x.startswith('_')])
-        ignore = 'encode decode format format_map isdecimal isdigit isprintable maketrans'
+        ignore = 'encode decode format_map isprintable maketrans'
         needed = needed.difference(ignore.split(' '))
         
         not_tested = needed.difference(tested)
diff --git a/pscript/tests/test_stdlib.py b/pscript/tests/test_stdlib.py
index c8aeac9..9d4222e 100644
--- a/pscript/tests/test_stdlib.py
+++ b/pscript/tests/test_stdlib.py
@@ -46,9 +46,9 @@ def test_stdlib_has_all_dict_methods():
 def test_stdlib_has_all_str_methods():
     method_names = [m for m in dir(str) if not m.startswith('_')]
     if sys.version_info[0] == 2:
-        ignore = 'encode decode format isdigit'
+        ignore = 'encode decode'
     else:
-        ignore = 'encode format format_map isdecimal isdigit isprintable maketrans'
+        ignore = 'encode format_map isprintable maketrans'
     for name in ignore.split(' '):
         method_names.remove(name)
     for method_name in method_names: