Repository: pallets/jinja (Python)

Issue:
scientific notation not supported in Python expressions
```
>>> import jinja2
>>> jinja2.__version__
'2.10'
>>> T = jinja2.Template('{{ 0.001 }}')
>>> T.render()
u'0.001'
>>> T = jinja2.Template('{{ 1e-3 }}')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "c:\app\python\anaconda\2\lib\site-packages\jinja2\environment.py", line 945, in __new__
    return env.from_string(source, template_class=cls)
  File "c:\app\python\anaconda\2\lib\site-packages\jinja2\environment.py", line 880, in from_string
    return cls.from_code(self, self.compile(source), globals, None)
  File "c:\app\python\anaconda\2\lib\site-packages\jinja2\environment.py", line 591, in compile
    self.handle_exception(exc_info, source_hint=source_hint)
  File "c:\app\python\anaconda\2\lib\site-packages\jinja2\environment.py", line 780, in handle_exception
    reraise(exc_type, exc_value, tb)
  File "<unknown>", line 1, in template
jinja2.exceptions.TemplateSyntaxError: expected token 'end of print statement', got 'e'
```


Discussion:
Jinja only supports decimal notation. The lexer would need to be updated to recognize scientific and other notations. I'm not sure the added complexity is necessary for templates. Is there a specific reason you need this?
I'm doing some log plots in HTML (base64 small png files, with a Python class `LinePlotter` I wrote to do the plots using matplotlib). Existing Jinja restrictions force me to use this in decimal notation, which suffers in readability for large magnitude values. Here's some sample content from a template I have:

```
{% set lineplotter = LinePlotter(width=7, dpi=90, xlimits=[0.01, 10000]) %}
{% for k,title,units,scaling in [
	('R','Resistance (R)','ohms (line-line)',2.0),
	('Lq','Inductance, q-axis (Lq)','mH (line-line)',2000.0),
	('Ld','Inductance, d-axis (Ld)','mH (line-line)',2000.0),
	('Ke','Back-emf (Ke)','Vrms/KRPM (line-line)',1.0/0.007797),
	('Tf','Friction (Tf)','mNm',1000.0),
	('B','Viscous damping (B)','uNm/(rad/s)',1000000.0),
	('J','Inertia (J)','uNm/(rad/s<sup>2</sup>)',1000000.0),
	('tau_e','Electrical time constant L/R','ms',1000.0),
	('tau_m1','Mechanical time constant 2/3 JR/Ke<sup>2</sup>','ms',1000.0)
] %}
{% set values = sample_motors['parameters'][k] * scaling %}
```

It would be more readable, and easier to avoid future errors during maintenance, for me to use `1.0e6` than `1000000.0`, or to use `xlimits=[1.0e-2, 1.0e4]` than `xlimits=[0.01, 10000.0]`.

These values aren't that large in magnitude, but in the engineering world we occasionally get into the 1e-12 range (pico) or the 1e9/1e12 (giga/tera) range. Decimal literals are prone to misreading/human error in that range.
That seems like processing that should be done in Python, passing the data to `render`.
I expected you to comment that.

No, it doesn't make sense, because I have parameters that are configured in the template for various situations. Other templates look at other aspects of the data model in different ways that are customizable by using different templates. My Python script is responsible for setting up the data model, and different templates show different views of it. Configuring the range of values shown on a log plot is well within the range of responsibility for the template. Yes, I could move it to a config file, and then have Python handle that part, but I use Jinja all the time for quick little scripting tasks like this, and it seems odd that I can do all these amazing things in Jinja templates (like the `for` loop in my example) but I can't use scientific notation. 

I have workarounds, so it's not blocking me from getting things done, but they're ugly. I'm an engineer; scientific notation is one of my basic tools and I can't use it here.



Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index ae7f9d601..0ee532418 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -21,6 +21,8 @@ unreleased
 - Added a ``default`` parameter for the ``map`` filter. (`#557`_)
 -   Exclude environment globals from
     :func:`meta.find_undeclared_variables`. #931
+-   Float literals can be written with scientific notation, like
+    ``{{ 2.56e-3 }}``. #912, #922
 
 .. _#557: https://github.com/pallets/jinja/issues/557
 .. _#765: https://github.com/pallets/jinja/issues/765
diff --git a/docs/templates.rst b/docs/templates.rst
index 6d3bf367b..88df91674 100644
--- a/docs/templates.rst
+++ b/docs/templates.rst
@@ -396,7 +396,7 @@ this template, it first locates the parent.  The extends tag should be the
 first tag in the template.  Everything before it is printed out normally and
 may cause confusion.  For details about this behavior and how to take
 advantage of it, see :ref:`null-master-fallback`. Also a block will always be
-filled in regardless of whether the surrounding condition is evaluated to be true 
+filled in regardless of whether the surrounding condition is evaluated to be true
 or false.
 
 The filename of the template depends on the template loader.  For example, the
@@ -1177,11 +1177,13 @@ for Python objects such as strings and numbers.  The following literals exist:
     arguments to function calls and filters, or just to extend or include a
     template).
 
-42 / 42.23:
-    Integers and floating point numbers are created by just writing the
-    number down.  If a dot is present, the number is a float, otherwise an
-    integer.  Keep in mind that, in Python, ``42`` and ``42.0``
-    are different (``int`` and ``float``, respectively).
+42:
+    Integers are whole numbers without a decimal part.
+
+42.23 / 42.1e2:
+    Floating point numbers can be written using a '.' as a decimal mark.
+    They can also be written in scientific notation with an upper or
+    lower case 'e' to indicate the exponent part.
 
 ['list', 'of', 'objects']:
     Everything between two brackets is a list.  Lists are useful for storing
diff --git a/jinja2/lexer.py b/jinja2/lexer.py
index e23a9987d..d8812c9b9 100644
--- a/jinja2/lexer.py
+++ b/jinja2/lexer.py
@@ -15,6 +15,7 @@
     :license: BSD, see LICENSE for more details.
 """
 import re
+from ast import literal_eval
 from collections import deque
 from operator import itemgetter
 
@@ -52,7 +53,7 @@
     del jinja2._identifier
     del _identifier
 
-float_re = re.compile(r'(?<!\.)\d+\.\d+')
+float_re = re.compile(r'(?<!\.)\d+(?:\.\d+)?(?:e[+\-]?\d+)?', re.IGNORECASE)
 newline_re = re.compile(r'(\r\n|\r|\n)')
 
 # internal the tokens and keep references to them
@@ -601,7 +602,7 @@ def wrap(self, stream, name=None, filename=None):
             elif token == 'integer':
                 value = int(value)
             elif token == 'float':
-                value = float(value)
+                value = literal_eval(value)
             elif token == 'operator':
                 token = operators[value]
             yield Token(lineno, token, value)


Test patch:
diff --git a/tests/test_filters.py b/tests/test_filters.py
index fd3a0cf9a..28d1f302f 100644
--- a/tests/test_filters.py
+++ b/tests/test_filters.py
@@ -136,9 +136,11 @@ def test_first(self, env):
     def test_float(self, env):
         tmpl = env.from_string('{{ "42"|float }}|'
                                '{{ "ajsghasjgd"|float }}|'
+                               '{{ "10e1"|float }}|'
+                               '{{ "10.5e-10"|float }}|'
                                '{{ "32.32"|float }}')
         out = tmpl.render()
-        assert out == '42.0|0.0|32.32'
+        assert out == '42.0|0.0|100.0|1.05e-09|32.32'
 
     def test_format(self, env):
         tmpl = env.from_string('''{{ "%s|%s"|format("a", "b") }}''')
diff --git a/tests/test_lexnparse.py b/tests/test_lexnparse.py
index f3adeb46f..454a67b06 100644
--- a/tests/test_lexnparse.py
+++ b/tests/test_lexnparse.py
@@ -336,9 +336,36 @@ def test_inop(self, env):
         tmpl = env.from_string('{{ 1 in [1, 2, 3] }}|{{ 1 not in [1, 2, 3] }}')
         assert tmpl.render() == 'True|False'
 
-    def test_literals(self, env):
-        tmpl = env.from_string('{{ [] }}|{{ {} }}|{{ () }}')
-        assert tmpl.render().lower() == '[]|{}|()'
+    @pytest.mark.parametrize("value", ("[]", "{}", "()"))
+    def test_collection_literal(self, env, value):
+        t = env.from_string("{{ %s }}" % value)
+        assert t.render() == value
+
+    @pytest.mark.parametrize("value", ("1", "123"))
+    def test_int_literal(self, env, value):
+        t = env.from_string("{{ %s }}" % value)
+        assert t.render() == value
+
+    @pytest.mark.parametrize(
+        "value",
+        (
+            "1.2",
+            "34.56",
+            ("1e0", "1.0"),
+            ("10e1", "100.0"),
+            ("2.5e100", "2.5e+100"),
+            "2.5e+100",
+            ("25.6e-10", "2.56e-09"),
+        ),
+    )
+    def test_float_literal(self, env, value):
+        if isinstance(value, tuple):
+            value, expect = value
+        else:
+            expect = value
+
+        t = env.from_string("{{ %s }}" % value)
+        assert t.render() == expect
 
     def test_bool(self, env):
         tmpl = env.from_string('{{ true and false }}|{{ false '