-1

I am studying flask.render_template(template_name_or_list, **context) and amazed by its ability to use the keywords from **context to get them evaluated in the template's {{ expression }}. I tried to implement this [concept][1] with RegEx, but only ok with simple variables:

import re

context = {"page_title" : "this is my title", "page_body" : "this is my body"}
Ks = list(context.keys())
newL = []
with open('homePage.html', 'r') as html:
    for line in html:
         for key in Ks:
            y = re.search(f"(.*){{{{[ \t]*{key}[ \t]*}}}}(.*)", line)
            if y:
                line = y.group(1) + context[key] + y.group(2)
         newL.append(line)
with open('homePage3.html', 'w') as html:
    html.writelines(newL)

I know flask {{arg}} can work on any type of variables and valid expressions like f.string's {arg} does.

return render_template('form.html', fruit=fruit(apples=23, oranges=32))

<p>I have {{ fruit.apples }} apples and {{ fruit.oranges }} oranges.</p>
<p>There are {{ fruit.apples + fruit.oranges }} fruits.</p>
<p>The vendor for apples is {{ fruit.vendor['apples'].name }}</p>
<p>The vendor for oranges is {{ fruit.vendor['oranges'].name }}</p>

My question is does it come up with "context['fruit'].vendor['oranges'].name" with the key fruit is replaced with its value or it uses some other methods to convert them to f.string or string.format?

  [1]: https://stackoverflow.com/questions/46725789/flask-render-template
Leon Chang
  • 669
  • 8
  • 12
  • 1
    Note that `flask` uses `jinja2` for templating. Please consider adding `jinja2` tag to your question. – Daweo Jun 03 '21 at 06:48
  • 1
    jinja supports some expressions that are similar to python https://jinja.palletsprojects.com/en/3.0.x/templates/#expressions to support this it works pretty similar to the python interpreter in that it parses the templates into nodes (lexing) and interpretes them, it's not a few lines of clever regex – vinzenz Jun 03 '21 at 09:26
  • 1
    https://github.com/pallets/jinja/tree/main/src/jinja2 – vinzenz Jun 03 '21 at 09:26

1 Answers1

0

From the document, Jinja template engine is very flexible. A template contains variables and/or expressions, which get replaced with values when a template is rendered. {{ ... }} for Expressions to print to the template output. Template variables are defined by the context dictionary passed to the template. You can use a dot (.) to access attributes of a variable. If a variable or attribute does not exist, you will get back an undefined value.

Call trace is found as follows:

render_templaate('temp.html', **context) in flask/templating.py which imports class Environment and class Template from jinja2/environment.py. ==>>

_render(template: Template, context: dict, app: "Flask")  ==>>

rv = template.render(context)  # rv will have the rendered string ==>>

def render(self, *args: t.Any, **kwargs: t.Any) # class Template in environment.py
{
    ctx = self.new_context(dict(*args, **kwargs))
    
    try:
        return concat(self.root_render_func(ctx))  # type: ignore
    except Exception:
        self.environment.handle_exception()
}

where ctx is a new Context class that has all the class Template environment such as jinja2 lexer, compiler, CodeGnerator, loader and

variable_start_string: str = VARIABLE_START_STRING, Defaults to ``'{{'``.
variable_end_string: str = VARIABLE_END_STRING, Defaults to ``'}}'``.

The strings marking the beginning and end of a print statement.     

The context/args variables such as 'fruit' object will be passed to the template via ctx.parent['fruit'].

If the object is missing in the context dict (args -> ctx.parent) or has no attribute, a jinja2.exception.UndefinedError will be raised.
The statement self.root_render_func(ctx) is the source code as far as I can trace.

The way it uses keyword argument **context is same as the String format() Method str.format(*args, **kwargs) as shown in the code below.

hourlyRate = { "Monday" : 20, "Tuesday" : 25, "Wednesday" : 30, "Thursday" : 35,
               "Friday" : 40, "Saturday" : 50, "Sunday" : 60    }
def weeklyPay(**weekly_hrs):
    sum = 0
    for i in weekly_hrs:
        sum += weekly_hrs[i] * hourlyRate[i]
    print(f"Weekly work hours = {weekly_hrs} \t=> Week pay = {sum}")

# Simulating the String format() Method. accessing arguments’ attributes
def sFormat(**cmplx):
    c1 = cmplx['c1']
    c2 = cmplx['c2']
    print('Inside sFormat c1 = %.2f + %.2fj' % (c1.real, c1.imag))
    print('Inside sFormat c2 = %.2f + %.2fj' % (c2.real, c2.imag))
    print(f'sFormat(**c): {c1} formed from real part {c1.real} and imaginary part {c1.imag}\n'
          f'sFormat(**c): {c2} formed from real part {c2.real} and imaginary part {c2.imag}')

if  __name__ == "__main__":
    # A function taking wild/variable number of keyword arguments
    weeklyPay(**{'Friday' : 7, "Tuesday" : 10, "Wednesday" : 6})
    weeklyPay(Monday = 10, Thursday = 6, Saturday = 4)
    weeklyPay(Saturday = 5, **{"Wednesday": 12, "Thursday": 7})
    weeklyPay(**{"Friday": 10, "Sunday": 5}, Wednesday = 8, Thursday = 10)
    print()
    #   str. format(*args, **kwargs)
    print('String formatting {c1} with real part {c1.real} and imaginary part {c1.imag}\n'
          'String formatting {c2} with real part {c2.real} and imaginary part {c2.imag}'
          .format(**{"c1":8+20j, "c2":19-7j}))
    print('String formatting {c1} with real part {c1.real} and imaginary part {c1.imag}\n'
          'String formatting {c2} with real part {c2.real} and imaginary part {c2.imag}'
          .format(c1 = 71 + 92j, c2 = 34 - 82j))
    #   Simulated string formatting
    sFormat(**{'c1':3+5j, 'c2':6-10j})
    sFormat(c1=30+50j, c2=15-25j)

Output:

Weekly work hours = {'Friday': 7, 'Tuesday': 10, 'Wednesday': 6}    => Week pay = 710
Weekly work hours = {'Monday': 10, 'Thursday': 6, 'Saturday': 4}    => Week pay = 610
Weekly work hours = {'Saturday': 5, 'Wednesday': 12, 'Thursday': 7}     => Week pay = 855
Weekly work hours = {'Friday': 10, 'Sunday': 5, 'Wednesday': 8, 'Thursday': 10}     => Week pay = 1290

String formatting (8+20j) with real part 8.0 and imaginary part 20.0
String formatting (19-7j) with real part 19.0 and imaginary part -7.0
String formatting (71+92j) with real part 71.0 and imaginary part 92.0
String formatting (34-82j) with real part 34.0 and imaginary part -82.0
Inside sFormat c1 = 3.00 + 5.00j
Inside sFormat c2 = 6.00 + -10.00j
sFormat(**c): (3+5j) formed from real part 3.0 and imaginary part 5.0
sFormat(**c): (6-10j) formed from real part 6.0 and imaginary part -10.0
Inside sFormat c1 = 30.00 + 50.00j
Inside sFormat c2 = 15.00 + -25.00j
sFormat(**c): (30+50j) formed from real part 30.0 and imaginary part 50.0
sFormat(**c): (15-25j) formed from real part 15.0 and imaginary part -25.0

On a side note, the AST tree structure helps me better understand compiler design. Below is a Python program for ast_node_visit.py.

class   Node:

    def __init__(self, value, level):
        self.value = value
        self.level = level
        self.children = []

    def child(self, value):
        child = Node(value, self.level + 1)
        self.children.append(child)
        return child

    def visit(self):
        for n in self.children:
            print(f'Node : {self.value}, child : {n.value} @ level {n.level}')
            n.visit()

a = Node('A', 0)
b = a.child('B')
c = a.child('C')
d = b.child('D')
e = b.child('E')
f = c.child('F')
g = c.child('G')
h = c.child('H')
i = e.child('I')
j = g.child('J')
a.visit()

output:

Node : A, child : B @ level 1
Node : B, child : D @ level 2
Node : B, child : E @ level 2
Node : E, child : I @ level 3
Node : A, child : C @ level 1
Node : C, child : F @ level 2
Node : C, child : G @ level 2
Node : G, child : J @ level 3
Node : C, child : H @ level 2

where "level" roughly aligns with the Python code's indentation. You can add the following 'main' function in your source code and check out the 'tree.body' attribute:

import ast
def main(srcCode):
    with    open(srcCode, 'r')  as source:
        tree    = ast.parse(source.read())
if  __name__    ==  '__main__':
    main(__file__)
Leon Chang
  • 669
  • 8
  • 12