macropy
macropy copied to clipboard
Block macro outputting a single-item body
It would be convenient if a block macro could return, when appropriate for some particular use case, just a single AST node as the transformed body. Currently the return type from a block macro must be list
.
A real use case for this is the autocurry macro in unpythonic. (Usage, implementation.)
Minimal (non-)working example:
main.py
:
from mwe import macros, mwe
with mwe:
print("hi")
print("ho")
mwe.py
:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from macropy.core.macros import Macros
macros = Macros()
@macros.block
def mwe(tree, **kw):
return tree[0] # return just one AST node --> crash
[edit] update link. [edit] update doc link.
but then any caller has to have an if statement to ask whether the returned thing was iterable or one node. why not keep consistent return types and always return a List<node>
?
Hmm, a very pythonic viewpoint.
The reason I opened this ticket is that this also came up over email, and @azazel75 said the behavior I described (a block macro requires returning a list also for a single item) sounded like a bug.
I'd be happy if this was just mentioned in the documentation (somewhere here?), or if the assert failure message explictly said that a list was expected. Currently, it says what it got, but not what it expected. The information is there, but you have to look at the stack trace.
After reviewing and testing the code, I can say that the block macros type expects the macro to return a sequence of trees, but from my test it doesn't "crash", the return value hits an assertion, as of now..
File "run.py", line 2, in <module>
import main
File "<frozen importlib._bootstrap>", line 971, in _find_and_load
File "<frozen importlib._bootstrap>", line 951, in _find_and_load_unlocked
File "<frozen importlib._bootstrap>", line 894, in _find_spec
File "/home/azazel/wip/macropy/macropy/macropy/core/import_hooks.py", line 147, in find_spec
code, tree = self.expand_macros(source, origin, spec)
File "/home/azazel/wip/macropy/macropy/macropy/core/import_hooks.py", line 111, in expand_macros
tree, source_code, modules).expand_macros()
File "/home/azazel/wip/macropy/macropy/macropy/core/macros.py", line 592, in expand_macros
tree = super().expand_macros(tree)
File "/home/azazel/wip/macropy/macropy/macropy/core/macros.py", line 399, in expand_macros
return self.walk_tree(tree)
File "/home/azazel/wip/macropy/macropy/macropy/core/macros.py", line 548, in walk_tree
self.walk_children(tree)
File "/home/azazel/wip/macropy/macropy/macropy/core/macros.py", line 513, in walk_children
new_value = self.walk_tree(old_value)
File "/home/azazel/wip/macropy/macropy/macropy/core/macros.py", line 548, in walk_tree
self.walk_children(tree)
File "/home/azazel/wip/macropy/macropy/macropy/core/macros.py", line 518, in walk_children
new_t = self.walk_tree(t)
File "/home/azazel/wip/macropy/macropy/macropy/core/macros.py", line 539, in walk_tree
new_tree = self.walk_tree(expand_it.send(new_tree))
File "/home/azazel/wip/macropy/macropy/macropy/core/macros.py", line 421, in macro_expand
mdata = type_it.send(new_tree)
File "/home/azazel/wip/macropy/macropy/macropy/core/macros.py", line 191, in detect_macro
assert isinstance(new_tree, list), type(new_tree)
AssertionError: <class '_ast.Expr'>
I may not be the best option to deal with this issue, but it's somewhat clear why the assert raises an error. I will think how to make it more clear
Yes, the assert isinstance(new_tree, list)
is the very last item in the stack trace, but Python has spoiled me to expect the error message to already contain all necessary information. :)
Maybe something like this?
assert isinstance(new_tree, list), "expected list, got {}".format(type(new_tree))
Update: during my investigation of #21, I noticed that in macropy.core.macros.Block.detect_macro
, there is already a special case to automatically wrap a single expression returned by a block macro into an ast.Expr
(as required for an expression in a statement position) and then pack that into a list
.
However, if the block macro returns a single statement, there's no special case to catch that, and it just errors out.
IMHO, this asymmetry feels unintentional, and likely an oversight from the original authors.
I think this would be more logical if there either were no special cases (Zen of Python 8, not special enough to break the rules), or if both a single expression and a single statement were special-cased (Zen of Python 9, practicality beats purity).
For me yours is a special case, it takes as list of statements as input it's natural to expect that it emit a list of statement. The ast.Expr trick is there for backward compatibility but no more exceptions will be added, by me.
I agree it's fine to expect a list of statements. Knowing that, there's no problem.
I didn't know the ast.Expr
trick is for backward compatibility only, as the code itself didn't suggest it is deprecated. Granted, it's not mentioned in the docs.
Ping? Should this be mentioned in the docs? I could write a PR with a suggestion?
If you think there's no need to document this, in that case I think it's fine to close this issue.