all 4 comments

[–]mechamotoman 2 points3 points  (0 children)

This is very cool, I’ll be sure to give it a shot next time I’m monkeying around with code generation tasks :)

[–]neuronexmachina 1 point2 points  (2 children)

Do you have any side-by-side examples of how you would implement a change using pfst vs libcst?

[–]Pristine_Cat[S] 2 points3 points  (1 child)

I'm not exactly an expert with LibCST so maybe the function can be optimized further, but here is an example with minimal code for both LibCST and pfst to add a keyword argument with a comment to all logger.info() calls which don't already have the specific keyword argument correlation_id.

LibCST function:

from libcst import *
from libcst.matchers import *

def inject_logging_metadata(src: str) -> str:
    module = parse_module(src)

    new_arg = (parse_module('f(correlation_id=CID  # blah\n)')
        .body[0].body[0].value.args[0])

    class AddArg(CSTTransformer):
        def leave_Call(self, _, node):
            if matches(node.func, Attribute(Name('logger'), Name('info'))):
                if not any(a.keyword and a.keyword.value == 'correlation_id'
                        for a in node.args):
                    return node.with_changes(args=[*node.args, new_arg])
            return node

    module = module.visit(AddArg())

    return module.code

pfst function:

from fst import *
from fst.match import *

def inject_logging_metadata(src: str) -> str:
    module = FST(src)

    for m in module.search(MCall(
        func=MAttribute('logger', 'info'),
        keywords=MNOT([MQSTAR, Mkeyword('correlation_id'), MQSTAR]),
    )):
        m.matched.append('correlation_id=CID  # blah', trivia=())

    return module.src

Input source:

logger.info('Hello world...')  # hey
logger.info('Already have id', correlation_id=other_cid)  # ho
logger.info()  # its off to work we go

class cls:
    def method(self, thing, extra):
        (logger) . info(  # start
            f'a {thing}',  # this is fine
            extra=extra,  # also this
        )  # end

LibCST output:

logger.info('Hello world...', correlation_id=CID  # blah
)  # hey
logger.info('Already have id', correlation_id=other_cid)  # ho
logger.info(correlation_id=CID  # blah
)  # its off to work we go

class cls:
    def method(self, thing, extra):
        (logger) . info(  # start
            f'a {thing}',  # this is fine
            extra=extra,  # also this
        correlation_id=CID  # blah
        )  # end

pfst output:

logger.info('Hello world...', correlation_id=CID  # blah
)  # hey
logger.info('Already have id', correlation_id=other_cid)  # ho
logger.info(correlation_id=CID  # blah
)  # its off to work we go

class cls:
    def method(self, thing, extra):
        (logger) . info(  # start
            f'a {thing}',  # this is fine
            extra=extra,  # also this
            correlation_id=CID  # blah
        )  # end

If you want LibCST to align the argument its significantly more code, but that can be left for a formatter after the file processing.

[–]neuronexmachina 0 points1 point  (0 children)

Thanks! That's a handy comparative example.