Debug Three Planted Bugs in the Mako Python Template Engine
StripeSoftware Engineer · Technical ScreenLast updated
A bug-squash exercise on a copy of the Mako Python template engine with planted bugs: lambda parameters vanish from regenerated code, a directory is mistaken for a template during lookup, and a comment-only control block fails to compile. It tests tracing failures through a template compiler, minimal library fixes and regression tests.
You are given a working copy of Mako, an open-source Python template engine, into which bugs have been planted. Find each bug, fix it with the smallest correct change to the library, and add a regression test for it.
Background: Mako compiles every template into a Python module. It parses the template into a tree of nodes (plain text, ${...} expressions, % control lines such as % if and % endif, ## comments, and tags such as <%def>), parses the embedded Python with the standard ast module, and generates the module's source code from both.
Three bugs are reported. Assume the failing tests show the symptoms described in the three parts below.
Constraints and Clarifications
- Fix the library, never the tests. You can run the test suite and any snippet you like.
- Treat each symptom as the result of one planted bug, and fix them one at a time.
- Mako can show the Python source it generates for a template; you may use it while debugging.
Clarifying Questions
- Am I expected to add regression tests, or only to make the existing tests pass?
- When a requested template name matches a directory in one search directory and a file in a later one, should the lookup return the later file?
- Should a control block whose body is only comments render nothing, or should the engine reject it with a clear error?
- Which Python versions must the fixes support? The
astmodule's node types differ between versions.
Part 1 — Parameters disappear from regenerated code
When Mako turns a parsed Python expression back into source code, the parameter names of a lambda are lost: lambda item: item.name comes back as a lambda with no parameters, lambda : item.name. A template function whose signature has such a default value still compiles, but calling the default fails when the template renders:
<%def name="listing(items, key=lambda item: item.name)">
...
</%def>
What This Part Should Cover
- How the missing text was traced to the code that handles one kind of
astnode - The root cause and the minimal fix
- A regression test that fails before the fix and passes after it
Part 2 — A directory is taken for a template
A template lookup is configured with several search directories. When the requested template name matches a directory inside one of them, the lookup tries to open that directory as a template and fails with an operating-system error such as IsADirectoryError, instead of moving on to the next search directory or raising the lookup's own "template not found" error.
What This Part Should Cover
- The check that accepts the wrong candidate, and the corrected check
- Behavior with several search directories, and when no directory has the template
- Related cases: symbolic links, and a file that disappears between the check and the open
Part 3 — A comment-only block fails to compile
This template fails to compile with a Python syntax error, although a block whose body is only a comment should simply produce no output:
% if show_footer:
## the footer is disabled for now
% endif
What This Part Should Cover
- Why a body made only of comments produces invalid Python
- The exact rule for when an empty body needs special handling, including
% elifand% elsebranches - Regression tests for each variant
Hints
Hint 1
Compare the tree with the text: Print the parsed tree of `lambda item: item.name` with `ast.dump` and compare it, node by node, with what the generator writes. Find the first node whose output is missing.
Hint 2
Reread the guard: Find the check the lookup performs before it accepts a candidate path, and list every kind of filesystem entry that passes it.
Hint 3
Compare with blocks that work: Compile the same block with a line of text in its body, and with nothing at all in its body, and see which of the three variants generates valid Python.
Follow-up questions
- No existing test exercised lambda parameters. What kind of test would catch any node type the source generator mishandles, not just this one?
- A new Python release adds a node type to the `ast` module. Should the generator silently skip a node it has no method for, or fail?
- Should the lookup reject a requested name such as `../config/secrets.txt` that resolves outside its search directories? How would you enforce it?
- A multi-line `<%doc>` comment is the only content of a control block. Does your Part 3 fix cover it?
Solution walkthrough is for members
Get the full walkthrough with reference code and complexity analysis, plus Kitty Coach at 300 messages a day. Hints, the code runner and community answers stay free.