Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Run mako templates as standalone executables #358

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
74 changes: 74 additions & 0 deletions doc/build/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,80 @@ which describes its general API:
print(line, "\n")
print("%s: %s" % (str(traceback.error.__class__.__name__), traceback.error))


Standalone Template Evaluation
==============================

Direct Template Rendering
-------------------------

You can evaluate a template file with the ``mako-render`` tool directly.
Alternatively, you can execute the `mako` Python package with ``python3 -m mako``.

All available options can be seen by invoking:

.. sourcecode:: sh
mako-render --help

By default, the template is read from ``stdin``, and results are written to ``stdout``.
To evaluate template files, pass the template file name, variables, and some arguments.

Suppose you have this ``mytemplate.mako``:

.. sourcecode:: mako

paper of the year by: ${name}!

You can then evaluate the template on command-line by calling

.. sourcecode:: sh

mako-render --var name="aperture science" mytemplate.mako


Template Execution
------------------

``mako-render`` can also be used for executable template files. Such a file may
be useful for dynamically generating static config files, especially in
conjunction with some kind of "execfs" which executes files when they are read.

Command line arguments given to the template can be evaluated within the template.

To use this feature, choose ``#!/usr/bin/env -S mako-render -s -a --`` as your
shebang. The ``-s`` will strip away the shebang (which is part of the template)
from the resulting output. Using ``--`` allows us to pass template cli arguments
starting with ``-``. Option ``-a`` enables passing cli arguments to the template
as ``sys.argv`` so the template can do arg-parsing on its own easily.


In this example, we have an executable text file named ``executabletemplate.mako``:

.. sourcecode:: mako

#!/usr/bin/env -S mako-render -s -a --
<%!
import argparse
cli = argparse.ArgumentParser()
cli.add_argument("someargument")
cli.add_argument("--test")
args = cli.parse_args()
%>\
templates can be executed: ${args.test} ${args.someargument}

Let's execute this template by running:

.. sourcecode:: sh

./executabletemplate.mako well! --test 'it works'

This will result in output:

.. sourcecode::

templates can be executed: it works well!


Common Framework Integrations
=============================

Expand Down
14 changes: 14 additions & 0 deletions mako/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/usr/bin/env python3

"""
entry point for directly running mako as templating engine.

that way, mako can directly be used as an rendering interpreter
in an executable text files' shebang:
#!/usr/bin/env -S python3 -m mako -s --
"""

from .cmd import cmdline

if __name__ == "__main__":
cmdline()
44 changes: 42 additions & 2 deletions mako/cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,33 @@ def cmdline(argv=None):
default=None,
help="Write to file upon successful render instead of stdout",
)
parser.add_argument("input", nargs="?", default="-")
parser.add_argument(
"--strip-shebang", "-s", action="store_true",
help="Strip a shebang in the first line of given input file",
)
parser.add_argument(
"--shift-args", "-a", action="store_true",
help="pass further `args` of this argument parser to the template"
"as sys.argv - allowing the template to do argparsing again.",
)
parser.add_argument("input", nargs="?", default="-",
help="file to parse as template. default is stdin.")
parser.add_argument("args", nargs="*",
help=("arguments passed to template's script. "
"use '-- args...' when first arg "
"starts with - or "))

options = parser.parse_args(argv)

output_encoding = options.output_encoding
output_file = options.output_file

# strip away mako's original args, so the template can get new args
# we do this so templates can handle further args on their own
if options.shift_args:
saved_sys_argv = sys.argv
sys.argv = [options.input] + options.args
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hey there -

what I want is:

  1. do not change sys.argv at all
  2. make a new template variable "sys_argv" with [options.input] + options.args, or whatever is most appropriate for argparse
  3. users that want to use argparse in a template use parser.parse_args(sys_argv)

that's it!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay great, I'll implement that :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll mark it resolved when the next patch is put up (with the issue resolved :) )


if options.input == "-":
lookup_dirs = options.template_dir or ["."]
lookup = TemplateLookup(lookup_dirs)
Expand All @@ -75,25 +95,45 @@ def cmdline(argv=None):
raise SystemExit("error: can't find %s" % filename)
lookup_dirs = options.template_dir or [dirname(filename)]
lookup = TemplateLookup(lookup_dirs)

preprocessor = None
if options.strip_shebang:
TheJJ marked this conversation as resolved.
Show resolved Hide resolved
def preprocessor(content):
if content.startswith("#!"):
lineend = content.find('\n')
# without newline after shebang, content will be unchanged
content = content[lineend + 1:]
return content

try:
template = Template(
filename=filename,
lookup=lookup,
output_encoding=output_encoding,
preprocessor=preprocessor,
)
except:
_exit()

kw = dict(varsplit(var) for var in options.var)
# template rendering args
kw = {
"template_argv": options.args,
} | dict(varsplit(var) for var in options.var)

try:
rendered = template.render(**kw)
except SystemExit:
pass
TheJJ marked this conversation as resolved.
Show resolved Hide resolved
except:
_exit()
else:
if output_file:
open(output_file, "wt", encoding=output_encoding).write(rendered)
else:
sys.stdout.write(rendered)
finally:
if options.shift_args:
sys.argv = saved_sys_argv


if __name__ == "__main__":
Expand Down
9 changes: 9 additions & 0 deletions test/templates/cmd_args.mako
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/usr/bin/env -S mako-render -s -a --
<%
import argparse
cli = argparse.ArgumentParser()
cli.add_argument("stuff")
cli.add_argument("--test")
args = cli.parse_args()
%>\
executable template ${args.stuff} ${args.test}
2 changes: 2 additions & 0 deletions test/templates/cmd_shebang.mako
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/usr/bin/env -S mako-render -s
executable template ${x}
31 changes: 31 additions & 0 deletions test/test_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,34 @@ def test_file_notfound(self):
SystemExit, "error: can't find fake.lalala"
):
cmdline(["--var", "x=5", "fake.lalala"])

def test_strip_shebang(self):
with self._capture_output_fixture() as stdout:
cmdline(
[
"--var",
"x=42",
"--strip-shebang",
os.path.join(config.template_base,
"cmd_shebang.mako"),
]
)

eq_(stdout.write.mock_calls[0][1][0], "executable template 42")

def test_template_arguments(self):
with self._capture_output_fixture() as stdout:
cmdline(
[
"-s", # short form for shebang strip
"-a", # replace sys.argv during rendering
os.path.join(config.template_base,
"cmd_args.mako"),
"--",
"--test=42",
"with args",
]
)

eq_(stdout.write.mock_calls[0][1][0],
"executable template with args 42")