In the previous article I wrote how-to add partial application with ...
and piping with @ using AST transformations. However we needed to
transform AST manually. For automatizing it I planned to use macropy
but it doesn’t work with Python 3 and a bit too complicated. So I ended up with
an idea to create __transformers__ module that work in a similar way with Python’s
__future__ module. So code will look like:
from __transformers__ import ellipsis_partial, matmul_pipe
range(10) @ map(lambda x: x ** 2, ...) @ list @ print
So first of all for implementing it we need to extract enabled transformers names
from code, it’s easy with ast.NodeVisitor, we just process all ImportForm nodes:
import ast
class NodeVisitor(ast.NodeVisitor):
def __init__(self):
self._found = []
def visit_ImportFrom(self, node):
if node.module == '__transformers__':
self._found += [name.name for name in node.names]
@classmethod
def get_transformers(cls, tree):
visitor = cls()
visitor.visit(tree)
return visitor._found
Let’s run it:
tree = ast.parse(code)
>>> print(NodeVisitor.get_transformers(tree))
['ellipsis_partial', 'matmul_pipe']
Next step is to define transformers. Transformer is just a Python module
with transformer variable, that is instance of ast.NodeTransformer.
For example transformer module for piping with matrix multiplication operator
will be like:
import ast
class MatMulPipeTransformer(ast.NodeTransformer):
def _replace_with_call(self, node):
"""Call right part of operation with left part as an argument."""
return ast.Call(func=node.right, args=[node.left], keywords=[])
def visit_BinOp(self, node):
if isinstance(node.op, ast.MatMult):
node = self._replace_with_call(node)
node = ast.fix_missing_locations(node)
return self.generic_visit(node)
transformer = MatMulPipeTransformer()
Now we can write function that extracts used transformers, imports and applies it to AST:
def transform(tree):
transformers = NodeVisitor.get_transformers(tree)
for module_name in transformers:
module = import_module('__transformers__.{}'.format(module_name))
tree = module.transformer.visit(tree)
return tree
And use it on our code:
from astunparse import unparse
>>> unparse(transform(tree))
from __transformers__ import ellipsis_partial, matmul_pipe
print(list((lambda __ellipsis_partial_arg_0: map((lambda x: (x ** 2)), __ellipsis_partial_arg_0))(range(10)))
Next part is to automatically apply transformations on module import, for that we need to
implement custom Finder and Loader. Finder is almost similar with
PathFinder, we just need to replace Loader with ours in spec. And
Loader is almost SourceFileLoader, but we need to run our transformations
in source_to_code method:
from importlib.machinery import PathFinder, SourceFileLoader
class Finder(PathFinder):
@classmethod
def find_spec(cls, fullname, path=None, target=None):
spec = super(Finder, cls).find_spec(fullname, path, target)
if spec is None:
return None
spec.loader = Loader(spec.loader.name, spec.loader.path)
return spec
class Loader(SourceFileLoader):
def source_to_code(self, data, path, *, _optimize=-1):
tree = ast.parse(data)
tree = transform(tree)
return compile(tree, path, 'exec',
dont_inherit=True, optimize=_optimize)
Then we need to put our finder in sys.meta_path:
import sys
def setup():
sys.meta_path.insert(0, Finder)
setup()
And now we can just import modules that use transformers. But it requires some bootstrapping.
We can make it easier by creating __main__ module that will register module
finder and run module or file:
from runpy import run_module
from pathlib import Path
import sys
from . import setup
setup()
del sys.argv[0]
if sys.argv[0] == '-m':
del sys.argv[0]
run_module(sys.argv[0])
else:
# rnupy.run_path ignores meta_path for first import
path = Path(sys.argv[0]).parent.as_posix()
module_name = Path(sys.argv[0]).name[:-3]
sys.path.insert(0, path)
run_module(module_name)
So now we can run our module easily:
➜ python -m __transformers__ -m test
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
➜ python -m __transformers__ test.py
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
And that’s all, you can try transformers by yourself with transformers package:
pip install transformers