macropy icon indicating copy to clipboard operation
macropy copied to clipboard

Block macro outputting a single-item body

Open Technologicat opened this issue 6 years ago • 8 comments

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.

Technologicat avatar Oct 02 '18 12:10 Technologicat

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>?

catb0t avatar Oct 03 '18 00:10 catb0t

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.

Technologicat avatar Oct 03 '18 12:10 Technologicat

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

azazel75 avatar Oct 04 '18 00:10 azazel75

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))

Technologicat avatar Oct 04 '18 12:10 Technologicat

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).

Technologicat avatar Oct 27 '18 21:10 Technologicat

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.

azazel75 avatar Oct 28 '18 13:10 azazel75

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.

Technologicat avatar Oct 28 '18 15:10 Technologicat

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.

Technologicat avatar Aug 18 '20 11:08 Technologicat