fst.docs.d12_examples

Example recipes

These are a few snippets which do some real-world-ish things. The comment handling blemishes are left in place and your actual mileage with comments may vary. It depends very much of correct usage of the trivia option both on get() and put(), and comment handling in general needs a bit more work (a lot more work), but they are preserved mostly.

The examples are deliberately not the most efficient but are rather meant to show off fst usage and features. Some of them are somewhat linter-y, which is not the intended use of this module, but fine for demonstration purposes.

To be able to execute the examples, import this.

>>> from fst import *

And this is just a print helper function for this documentation specifically, you can ignore it.

>>> def pprint(src):  # helper
...    print(src.replace('\n\n', '\n\xa0\n'))  # replace() to avoid '<BLANKLINE>'

else if chain to elif

fst has elif <-> else if code built in as its needed for statement insertions and deletions from conditional bodies so its fairly easy to leverage to change these kinds of chains.

>>> src = r"""
... def func():
...     # pre-if-a
...     if a:  # if-a
...         # pre-i
...         i = 1  # i
...         # post-i
...
...     else:  # else-a
...         # pre-if-b
...         if b:  # if-b
...             # pre-j
...             j = 2  # j
...             # post-j
...
...         else:  # else-b
...             # pre-if-c
...             if c:  # if-c
...                 # pre-k
...                 k = 3  # k
...                 # post-k
...
...             else:  # else-c
...                 # pre-l
...                 l = 4  # l
...                 # post-l
...             # post-else-c
...
...         # post-else-b
...
...     # post-else-a
... """.strip()

Function.

>>> def else_if_chain_to_elifs(src):
...     fst = FST(src, 'exec')
...
...     for f in fst.walk():
...         if (f.is_elif() is False           # False means normal `if`, not an `elif`
...             and f.parent.is_If             # in a parent `if`
...             and f.pfield == ('orelse', 0)  # as first element of `.orelse` body
...             and len(f.parent.orelse) == 1  # which has only this node
...         ):
...             f.replace(  # can replace while walking
...                 f.copy(trivia=('block', 'all')),
...                 trivia=(False, 'all'),
...                 elif_=True,  # elif_=True is default, here to show usage
...             )
...
...     return fst.src

Original.

>>> pprint(src)
def func():
    # pre-if-a
    if a:  # if-a
        # pre-i
        i = 1  # i
        # post-i
 
    else:  # else-a
        # pre-if-b
        if b:  # if-b
            # pre-j
            j = 2  # j
            # post-j
 
        else:  # else-b
            # pre-if-c
            if c:  # if-c
                # pre-k
                k = 3  # k
                # post-k
 
            else:  # else-c
                # pre-l
                l = 4  # l
                # post-l
            # post-else-c
 
        # post-else-b
 
    # post-else-a

Processed.

>>> pprint(else_if_chain_to_elifs(src))
def func():
    # pre-if-a
    if a:  # if-a
        # pre-i
        i = 1  # i
        # post-i
 
    # pre-if-b
    elif b:  # if-b
        # pre-j
        j = 2  # j
        # post-j
 
    # pre-if-c
    elif c:  # if-c
        # pre-k
        k = 3  # k
        # post-k
 
    else:  # else-c
        # pre-l
        l = 4  # l
        # post-l
    # post-else-c
 
    # post-else-b
 
    # post-else-a

elif chain to else if

This is just the inverse of the else if to elif code, just as easy.

>>> src = r"""
... def func():
...     # pre-if-a
...     if a:  # if-a
...         # pre-i
...         i = 1  # i
...         # post-i
...
...     # pre-if-b
...     elif b:  # if-b
...         # pre-j
...         j = 2  # j
...         # post-j
...
...     # pre-if-c
...     elif c:  # if-c
...         # pre-k
...         k = 3  # k
...         # post-k
...
...     else:  # else-c
...         # pre-l
...         l = 4  # l
...         # post-l
...     # post-else-c
...
...     # post-else-b
...
...     # post-else-a
... """.strip()

Function.

>>> def elif_chain_to_else_ifs(src):
...     fst = FST(src, 'exec')
...
...     for f in fst.walk():
...         if (f.is_elif()                    # True means `elif`
...             and f.parent.is_If             # in a parent `if`
...             and f.pfield == ('orelse', 0)  # as first element of `.orelse` body
...             and len(f.parent.orelse) == 1  # which has only this node
...         ):
...             f.replace(  # can replace while walking
...                 f.copy(trivia=('block', 'block')),
...                 trivia=('block', 'block'),
...                 elif_=False,
...             )
...
...     return fst.src

Original.

>>> pprint(src)
def func():
    # pre-if-a
    if a:  # if-a
        # pre-i
        i = 1  # i
        # post-i
 
    # pre-if-b
    elif b:  # if-b
        # pre-j
        j = 2  # j
        # post-j
 
    # pre-if-c
    elif c:  # if-c
        # pre-k
        k = 3  # k
        # post-k
 
    else:  # else-c
        # pre-l
        l = 4  # l
        # post-l
    # post-else-c
 
    # post-else-b
 
    # post-else-a

Processed.

>>> pprint(elif_chain_to_else_ifs(src))
def func():
    # pre-if-a
    if a:  # if-a
        # pre-i
        i = 1  # i
        # post-i
 
    else:
        # pre-if-b
        if b:  # if-b
            # pre-j
            j = 2  # j
            # post-j
 
        else:
            # pre-if-c
            if c:  # if-c
                # pre-k
                k = 3  # k
                # post-k
 
            else:  # else-c
                # pre-l
                l = 4  # l
                # post-l
            # post-else-c
 
    # post-else-b
 
    # post-else-a

lambda to def

Maybe you have too many lambdas and want proper function defs for debugging or logging or other tools. Note the defs are left in the same scope in case of nonlocals.

>>> src = r"""
... # pre-lambda comment
... mymin = lambda a, b: a if a < b else b  # inline lambda comment
... # post-lambda comment
...
... class cls:
...     name = lambda self: str(self)
...
...     def method(self, a, b):
...         add = lambda: a + b
...
...         return add()
... """.strip()

Function.

>>> def lambdas_to_defs(src):
...     fst = FST(src, 'exec')
...
...     for f in fst.walk():
...         if (f.is_Assign
...             and f.value.is_Lambda
...             and f.targets[0].is_Name
...             and len(f.targets) == 1   # for demo purposes just deal with this case
...         ):
...             flmb = f.value
...             fdef = FST(f"""
... def {f.targets[0].id}({flmb.args.src}):
...     return _
...             """.strip())  # template
...             fdef.body[0].value = flmb.body.copy()
...
...             f.replace(
...                 fdef,
...                 trivia=(False, 'line'),  # eat line comment but not leading
...                 pep8space=1,  # don't doublespace inserted func def (at mod scope)
...             )
...
...     return fst.src

Original.

>>> pprint(src)
# pre-lambda comment
mymin = lambda a, b: a if a < b else b  # inline lambda comment
# post-lambda comment
 
class cls:
    name = lambda self: str(self)
 
    def method(self, a, b):
        add = lambda: a + b
 
        return add()

Processed.

>>> pprint(lambdas_to_defs(src))
# pre-lambda comment
def mymin(a, b):
    return a if a < b else b
 
# post-lambda comment
 
class cls:
    def name(self):
        return str(self)
 
    def method(self, a, b):
        def add():
            return a + b
 
        return add()

squash nested withs

Slice operations make this easy enough. We only do synchronous with here as you can't mix sync with async anyway.

>>> src = r"""
... # pre-with comment
... with open(a) as f:
...     with (
...         lock1,  # first lock
...         func() as lock2,  # this doesn't get preserved
...     ):
...         with ctx():  # this does not belong to ctx()
...             # body comment
...             pass
...             # end body comment
...
... # post-with comment
... """.strip()

Function.

>>> def squash_nested_withs(src):
...     fst = FST(src, 'exec')
...
...     for f in reversed([f for f in fst.walk() if f.is_stmt]):
...         if (f.is_With              # we are a `with`, we don't do `async with` here
...             and f.parent.is_With   # parent is another `with`
...             and f.pfield.idx == 0  # we are first child (we know we are in `.body`)
...         ):
...             f.parent.items.extend(f.items.copy())
...             f.parent.put_slice(
...                 # we cut to remove previous comments since not overwriting
...                 f.get_slice(trivia=('all+', 'block'), cut=True),
...                 trivia=(False, False),
...             )
...
...     return fst.src

Original.

>>> pprint(src)
# pre-with comment
with open(a) as f:
    with (
        lock1,  # first lock
        func() as lock2,  # this doesn't get preserved
    ):
        with ctx():  # this does not belong to ctx()
            # body comment
            pass
            # end body comment
 
# post-with comment

Processed.

>>> pprint(squash_nested_withs(src))
# pre-with comment
with (open(a) as f,
     lock1,  # first lock
     func() as lock2, ctx()
     ):
    # body comment
    pass
    # end body comment
 
# post-with comment

comprehension to loop

We build up a body and replace the original comprehension Assign statement with the new statements.

>>> src = r"""
... def f(k):
...     # safe comment
...     clean = [i for i in k]
...
...     # happy comment
...     messy = [
...         ( i )  # weird pars
...         for (
...             j
...         ) in k  # outer loop
...         if
...         j  # misc comment
...         and not validate(j)
...         for i in j  # inner loop
...         if i
...         if validate(i)
...     ]
...     # silly comment
...
...     return clean + messy
... """.strip()

Function.

>>> def list_comprehensions_to_loops(src):
...     fst = FST(src, 'exec')
...
...     for f in fst.walk():
...         if (f.is_Assign
...             and f.value.is_ListComp
...             and f.targets[0].is_Name
...             and len(f.targets) == 1
...         ):
...             var = f.targets[0].id
...             fcomp = f.value
...             fcur = ftop = FST(f'{var} = []\n_', 'exec')
...             # the `_` will become first `for`
...
...             for fgen in fcomp.generators:
...                 ffor = FST('for _ in _:\n    _')  # for loop, just copy the source
...                 ffor.target = fgen.target.copy()
...                 ffor.iter = fgen.iter.copy()
...
...                 fcur = fcur.body[-1].replace(ffor)
...                 fifs = fgen.ifs
...                 nifs = len(fifs)
...
...                 if nifs:  # if no ifs then no test
...                     if nifs == 1:  # if single test then just use that
...                         ftest = fifs[0].copy()
...
...                     else:  # if multiple then join with `and`
...                         ftest = FST(' and '.join('_' * nifs))
...
...                         for i, fif in enumerate(fifs):
...                             ftest.values[i] = fif.copy()
...
...                     fifstmt = FST('if _:\n    _')
...                     fifstmt.test = ftest
...
...                     fcur = fcur.body[-1].replace(fifstmt)
...
...             # the ffor is the last one processed above (the innermost)
...             fcur.body[-1].replace(f'{var}.append({fcomp.elt.copy().src})')
...
...             f.replace(
...                 ftop,
...                 one=False,  # this allows to replace a single element with multiple
...                 trivia=(False, False)
...             )
...
...     return fst.src

Original.

>>> pprint(src)
def f(k):
    # safe comment
    clean = [i for i in k]
 
    # happy comment
    messy = [
        ( i )  # weird pars
        for (
            j
        ) in k  # outer loop
        if
        j  # misc comment
        and not validate(j)
        for i in j  # inner loop
        if i
        if validate(i)
    ]
    # silly comment
 
    return clean + messy

Processed.

>>> pprint(list_comprehensions_to_loops(src))
def f(k):
    # safe comment
    clean = []
    for i in k:
        clean.append(i)
 
    # happy comment
    messy = []
    for j in k:
        if (j  # misc comment
            and not validate(j)):
            for i in j:
                if i and validate(i):
                    messy.append(i)
    # silly comment
 
    return clean + messy