Exporting your Expanded Code¶
Although MacroPy is designed to work seamlessly on-line, seamlessly translating your code on the fly as it gets imported, without having to trouble the programmer with a multi-stage expansion/execution process. However, there are use reasons for performing an explicit expansion:
- performance: walking the AST takes time, which may grow unbearable as the amount of code grows large. Pre-compiling (or at least caching) the macro-expanded code would save some frustration;
- deployment: you may be deploying your code in a Python environment where MacroPy doesn’t function (e.g. Jython), or you may want to package your code as a library without forcing your users to have a dependency on MacroPy;
- debugging: although MacroPy provides tools to help figure out what’s happening when things go wrong (e.g. show_expanded) it may sometimes to easier just to take a compile source dump of the entire source-tree after macro expansion so you can debug it directly, rather than through the expansion process.
MacroPy allows you to hook into the macro-expansion process via the
macropy.exporter
variable, which comes with three bundled values
which can satisfy these constraints:
- NullExporter(): this is the default exporter, which does nothing;
- SaveExporter(target, root): this saves
a copy of your code tree (rooted at
root
), with macros expanded, in thetarget
directory. This is a convenient way of exporting the entire source tree with macros expanded; - PycExporter(): this emulates the normal
.pyc
compilation and caching based on filemtime
. This is a convenient transparent-ish cache to avoid needlessly performing macro-expansion repeatedly.
NullExporter()¶
This is the default Exporter, and although it does not do anything, it illustrates the general contract of what an Exporter must look like:
class NullExporter(object):
def find(self, file, pathname, description, module_name, package_path):
pass
def export_transformed(self, code, tree, module_name, file_name):
pass
In short, it has two methods: find
and export_transformed
:
find
is called after a file has been loaded and the use of macros have been detected inside. It can either returnNone
, in which case macro-expansion goes ahead, or amodule
object, in which case macro-expansion is simply skipped and the returnedmodule
object is used instead;export_transformed
is called after macro-expansion has been successfully completed (It is not triggered on failures). Whatever it returns doesn’t matter.
The arguments to these methods are relatively self explanatory, but
feel free to inject print
statements into NullExporter
if you
want to see what’s what.
SaveExporter(target, root)¶
This exporter is activated immediately after the initial import
macropy.activate
statement, via:
import macropy.activate
macropy.exporter = SaveExporter("exported", ".")
It creates a copy of your source tree (rooted at root
) in the
target
directory, and any file which is macro-expanded will have its
expanded representation saved in that directory. For example, if you
have a project:
run.py
my_macro.py
file.py
stuff/
thing.py
Assuming run.py
is the entry point containing the import
macropy.activate
statement, we need to:
- modify it, as shown above, to contain the
macropy.exporter = SaveExporter(..., ...)
line; - run it, via
python run.py
or similar.
run.py
my_macro.py
file.py
stuff/
thing.py
saved/
run.py
my_macro.py
file.py
stuff/
thing.py
Where all macros within the files in the saved/
subdirectory which
were executed in the course of execution have been expanded. You can
verify this by removing the import macropy.activate
and
macropy.exporter = ...
lines from saved/run.py
(Thereby disabling
MacroPy) and executing saved/run.py
directly. Everything should run
as normal, demonstrating that all macros have been expanded the
dependencies on MacroPy’s import hooks and AST transformations have
been removed.
Note that only macros in files which get expanded in the execution of the program will have their expanded versions saved. This allows you to control which files you want to perform the macro-expansion-and-save on: for example, most projects have utility scripts which cannot be imported from the root, or example files which are similarly not directly importable.
In most cases, activating the SaveExporter
and executing your test
suite should cause all files necessary to be imported, expanded and
saved. If you need more customization, you could easily create a
script that performs exactly the imports you need, or imports all
modules in a folder, or any other behavior your want.
Pre-expanding the MacroPy Test Suite¶
The following example can be used to expand-and-save MacroPy’s own test suite, such that it can be run without macros:
# run_tests.py
import unittest
import macropy.activate
from macropy.core.exporters import SaveExporter
macropy.exporter = SaveExporter("exported", ".")
import macropy.test
unittest.TextTestRunner().run(macropy.test.Tests)
MacroPy’s test suite clearly makes extremely extensive use of
macros. Nevertheless, activating SaveExporter
before running the
test suite makes a copy of the entire source-tree with all macros
expanded; inspecting any of the previously-macro-using files in the
newly-created exported/
directory demonstrates that the macros have
really, truly, been expanded:
# exported/macropy/string_interp.py
from pickle import loads as sym1
import re
from macropy.core.macros import Macros
from macropy.core.hquotes import macros, u, ast_list
macros = Macros()
@macros.expr
def s(tree, **kw):
captured = []
new_string = ''
chunks = re.split('{(.*?)}', tree.s)
for i in range(0, len(chunks)):
if ((i % 2) == 0):
new_string += chunks[i]
else:
new_string += '%s'
captured += [chunks[i]]
result = BinOp(left=ast_repr(new_string), op=Mod(), right=Call(func=Captured(tuple, 'tuple'), args=[List(elts=map(parse_expr, captured))], keywords=[], starargs=None, kwargs=None))
return result
We can disable MacroPy’s runtime transformations completely by removing the import hook:
# exported/macropy/__init__.py
import sys
import core.import_hooks
import core.exporters
import os
# sys.meta_path.append(core.import_hooks.MacroFinder)
__version__ = "0.2.0"
exporter = core.exporters.NullExporter()
And when we run the saved, macro-expanded, macro-less version via cd
exported; python run_tests.py
:
----------------------------------------------------------------------
Ran 76 tests in 0.150s
FAILED (failures=4, errors=1)
A few minor failures, mainly in the error-message/line-numbers tests, as the pre-expanded code will have different line numbers than the just-in-time-expanded ASTs. Nonetheless, on the whole it works.
The SaveExporter should be of great help to any library-author who wants to use Macros internally (e.g. Case Classes to simplify class declarations, or MacroPEG Parser Combinators to write a parser) but does not want to saddle users of the library with having to activate import hooks, or wants to run the code in an environment where such functionality is not supported (e.g. Jython).
By using the SaveExporter
, the macro-using code is expanded into
plain Python, and although it may rely on MacroPy as a library
(e.g. the CaseClass
class in macropy/peg.py)
it won’t need any of MacroPy’s import-code-intercepting
AST-transforming capabilities at run-time.
PycExporter()¶
Warning
Due to changes in the way compiled source files are stored, PycExporter is not yet functional in MacroPy3.
The PycExporter makes MacroPy perform the same *.pc -> *.pyc
caching
that the normal Python import process does. This can be activated via:
import macropy.activate
macropy.exporter = PycExporter()
The macro-expansion process takes significantly longer than normal imports, and this may be helpful if you have a large number of large files using macros and you want to save having to re-expand them every execution.
Although PycExporter
automatically does the recompilation of the
macro-expanded files when they are modified, it notably does not do
recompilation of the macro-expanded files when the macros are
modified. This means that PycExporter
is not useful when doing
development on the macros themselves, since the output files will not
get properly recompiled when the macros change. For now it is best to
simply use the NullExporter() when messing with your
macros, and only using the PycExporter() when your
macros are stable and you are working on the target code.