Skip to content

Commit b497021

Browse files
committed
macro code: convert prefix tests to unpythonic.test.fixtures
This required changes to the block macro `prefix`, which usually rewrites tuples as function calls. It needed a new special case to leave alone the tuple that forms the arguments for the test macro. (It now leaves the tuple itself alone, but recurses into its elements.)
1 parent 0de4c4b commit b497021

File tree

3 files changed

+122
-100
lines changed

3 files changed

+122
-100
lines changed

unpythonic/syntax/prefix.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@
44
Experimental, not for use in production code.
55
"""
66

7-
from ast import Name, Call, Tuple, Load
7+
from ast import Name, Call, Tuple, Load, Index
88

99
from macropy.core.quotes import macros, q, u, ast_literal # noqa: F811, F401
1010
from macropy.core.walkers import Walker
1111

1212
from .letdoutil import islet, isdo, UnexpandedLetView, UnexpandedDoView
13+
from .testutil import isunexpandedtestmacro
1314

1415
from ..it import flatmap, rev, uniqify
1516

@@ -42,6 +43,20 @@ def transform(tree, *, quotelevel, set_ctx, stop, **kw):
4243
view = UnexpandedDoView(tree)
4344
view.body = [transform.recurse(expr, quotelevel=quotelevel) for expr in view.body]
4445
return tree
46+
47+
# integration with testing framework
48+
if isunexpandedtestmacro(tree):
49+
stop()
50+
if type(tree.slice) is not Index:
51+
assert False, "prefix: Slice and ExtSlice not implemented in analysis of testing macro arguments"
52+
body = tree.slice.value
53+
if type(body) is Tuple:
54+
# skip the transformation of the argument tuple itself, but transform its elements
55+
body.elts = [transform.recurse(expr, quotelevel=quotelevel) for expr in body.elts]
56+
else:
57+
tree.slice.value = transform.recurse(tree.slice.value, quotelevel=quotelevel)
58+
return tree
59+
4560
# general case
4661
# macro-created nodes might not have a ctx, but we run in the first pass.
4762
if not (type(tree) is Tuple and type(tree.ctx) is Load):

unpythonic/syntax/test/test_prefix.py

Lines changed: 95 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -3,102 +3,108 @@
33
44
**CAUTION**: Experimental, **not** recommended for use in production code."""
55

6-
# Use "with show_expanded:" to see what it did.
7-
#from macropy.tracing import macros, show_expanded
6+
from ...syntax import macros, test, test_raises # noqa: F401
7+
from ...test.fixtures import testset, returns_normally
88

9-
from ...syntax import macros, prefix, q, u, kw, curry, let, do # noqa: F401
9+
from ...syntax import macros, prefix, q, u, kw, curry, let, do # noqa: F401, F811
1010

1111
from ...fold import foldr
1212
from ...fun import composerc as compose, apply
1313
from ...llist import cons, nil, ll
1414

15+
# Use "with show_expanded:" to see what it did.
16+
#from macropy.tracing import macros, show_expanded
17+
1518
def runtests():
16-
with prefix:
17-
(print, "hello world")
18-
x = 42 # can write any regular Python, too
19-
20-
# quote operator q locally turns off the function-call transformation:
21-
t1 = (q, 1, 2, (3, 4), 5) # q takes effect recursively
22-
t2 = (q, 17, 23, x) # unlike in Lisps, x refers to its value even in a quote
23-
(print, t1, t2)
24-
25-
# unquote operator u locally turns the transformation back on:
26-
t3 = (q, (u, print, 42), (print, 42), "foo", "bar")
27-
assert t3 == (q, None, (print, 42), "foo", "bar")
28-
29-
# quotes nest; call transformation made when quote level == 0
30-
t4 = (q, (print, 42), (q, (u, u, print, 42)), "foo", "bar")
31-
assert t4 == (q, (print, 42), (None,), "foo", "bar")
32-
33-
# Be careful:
34-
try:
35-
(x,) # in a prefix block, this means "call the 0-arg function x"
36-
except TypeError:
37-
pass # 'int' object is not callable
38-
else:
39-
assert False, "should have attempted to call x"
40-
(q, x) # OK!
41-
42-
# give named args with kw(...) [it's syntax, not really a function!]:
43-
def f(*, a, b):
44-
return (q, a, b)
45-
# in one kw(...), or...
46-
assert (f, kw(a="hi there", b="foo")) == (q, "hi there", "foo")
47-
# in several kw(...), doesn't matter
48-
assert (f, kw(a="hi there"), kw(b="foo")) == (q, "hi there", "foo")
49-
# in case of duplicate name across kws, rightmost wins
50-
assert (f, kw(a="hi there"), kw(b="foo"), kw(b="bar")) == (q, "hi there", "bar")
51-
52-
# give *args with unpythonic.fun.apply, like in Lisps:
53-
lst = [1, 2, 3]
54-
def g(*args, **kwargs):
55-
return args + tuple(sorted(kwargs.items()))
56-
assert (apply, g, lst) == (q, 1, 2, 3)
57-
# lst goes last; may have other args first
58-
assert (apply, g, "hi", "ho", lst) == (q, "hi", "ho", 1, 2, 3)
59-
# named args in apply are also fine
60-
assert (apply, g, "hi", "ho", lst, kw(myarg=4)) == (q, "hi", "ho", 1, 2, 3, ('myarg', 4))
61-
62-
# Function call transformation only applies to tuples in load context
63-
# (i.e. NOT on the LHS of an assignment)
64-
a, b = (q, 100, 200)
65-
assert a == 100 and b == 200
66-
a, b = (q, b, a) # pythonic swap in prefix syntax; must quote RHS
67-
assert a == 200 and b == 100
68-
69-
# prefix leaves alone the let binding syntax ((name0, value0), ...)
70-
a = let((x, 42))[x << x + 1]
71-
assert a == 43
72-
73-
# but the RHSs of the bindings are transformed normally:
74-
def double(x):
75-
return 2 * x
76-
a = let((x, (double, 21)))[x << x + 1]
77-
assert a == 43
78-
79-
# similarly, prefix leaves the "body tuple" of a do alone
80-
# (syntax, not semantically a tuple), but recurses into it:
81-
a = do[1, 2, 3]
82-
assert a == 3
83-
a = do[1, 2, (double, 3)]
84-
assert a == 6
85-
86-
# the extra bracket syntax has no danger of confusion, as it's a list, not tuple
87-
a = let((x, 3))[[
88-
1,
89-
2,
90-
(double, x)]]
91-
assert a == 6
92-
93-
# Introducing the LisThEll programming language: an all-in-one solution with
94-
# the prefix syntax of Lisp, the speed of Python, and the readability of Haskell!
95-
with prefix, curry:
96-
mymap = lambda f: (foldr, (compose, cons, f), nil)
97-
double = lambda x: 2 * x
98-
(print, (mymap, double, (q, 1, 2, 3)))
99-
assert (mymap, double, (q, 1, 2, 3)) == ll(2, 4, 6)
100-
101-
print("All tests PASSED")
19+
with testset("unpythonic.syntax.prefix"):
20+
with prefix:
21+
(print, "hello world")
22+
x = 42 # can write any regular Python, too
23+
24+
with testset("quote operator"):
25+
# quote operator q locally turns off the function-call transformation:
26+
t1 = (q, 1, 2, (3, 4), 5) # q takes effect recursively
27+
t2 = (q, 17, 23, x) # unlike in Lisps, x refers to its value even in a quote
28+
(print, t1, t2)
29+
30+
# unquote operator u locally turns the transformation back on:
31+
t3 = (q, (u, print, 42), (print, 42), "foo", "bar")
32+
test[t3 == (q, None, (print, 42), "foo", "bar")]
33+
34+
# quotes nest; call transformation made when quote level == 0
35+
t4 = (q, (print, 42), (q, (u, u, print, 42)), "foo", "bar")
36+
test[t4 == (q, (print, 42), (None,), "foo", "bar")]
37+
38+
with testset("tuple in load context denotes a call"):
39+
# Be careful:
40+
test_raises[TypeError,
41+
(x,), # in a prefix block, this 1-element tuple means "call the 0-arg function x"
42+
"should have attempted to call x"]
43+
44+
test[returns_normally((q, x))] # quoted, OK!
45+
46+
# Function call transformation only applies to tuples in load context
47+
# (i.e. NOT on the LHS of an assignment)
48+
a, b = (q, 100, 200)
49+
test[a == 100 and b == 200]
50+
a, b = (q, b, a) # pythonic swap in prefix syntax; must quote RHS
51+
test[a == 200 and b == 100]
52+
53+
with testset("kwargs"):
54+
# give named args with kw(...) [it's syntax, not really a function!]:
55+
def f(*, a, b):
56+
return (q, a, b)
57+
# in one kw(...), or...
58+
test[(f, kw(a="hi there", b="foo")) == (q, "hi there", "foo")]
59+
# in several kw(...), doesn't matter
60+
test[(f, kw(a="hi there"), kw(b="foo")) == (q, "hi there", "foo")]
61+
# in case of duplicate name across kws, rightmost wins
62+
test[(f, kw(a="hi there"), kw(b="foo"), kw(b="bar")) == (q, "hi there", "bar")]
63+
64+
with testset("starargs"):
65+
# give *args with unpythonic.fun.apply, like in Lisps:
66+
lst = [1, 2, 3]
67+
def g(*args, **kwargs):
68+
return args + tuple(sorted(kwargs.items()))
69+
test[(apply, g, lst) == (q, 1, 2, 3)]
70+
# lst goes last; may have other args first
71+
test[(apply, g, "hi", "ho", lst) == (q, "hi", "ho", 1, 2, 3)]
72+
# named args in apply are also fine
73+
test[(apply, g, "hi", "ho", lst, kw(myarg=4)) == (q, "hi", "ho", 1, 2, 3, ('myarg', 4))]
74+
75+
with testset("integration with let and do"):
76+
# prefix leaves alone the let binding syntax ((name0, value0), ...)
77+
a = let((x, 42))[x << x + 1]
78+
test[a == 43]
79+
80+
# but the RHSs of the bindings are transformed normally:
81+
def double(x):
82+
return 2 * x
83+
a = let((x, (double, 21)))[x << x + 1]
84+
test[a == 43]
85+
86+
# similarly, prefix leaves the "body tuple" of a do alone
87+
# (syntax, not semantically a tuple), but recurses into it:
88+
a = do[1, 2, 3]
89+
test[a == 3]
90+
a = do[1, 2, (double, 3)]
91+
test[a == 6]
92+
93+
# the extra bracket syntax has no danger of confusion, as it's a list, not tuple
94+
a = let((x, 3))[[
95+
1,
96+
2,
97+
(double, x)]]
98+
test[a == 6]
99+
100+
# Introducing the LisThEll programming language: an all-in-one solution with
101+
# the prefix syntax of Lisp, the speed of Python, and the readability of Haskell!
102+
with testset("LisThEll"):
103+
with prefix, curry:
104+
mymap = lambda f: (foldr, (compose, cons, f), nil)
105+
double = lambda x: 2 * x
106+
(print, (mymap, double, (q, 1, 2, 3)))
107+
test[(mymap, double, (q, 1, 2, 3)) == ll(2, 4, 6)]
102108

103109
if __name__ == '__main__':
104110
runtests()

unpythonic/syntax/testutil.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,20 +30,21 @@
3030
_test_function_names = ["unpythonic_assert",
3131
"unpythonic_assert_signals",
3232
"unpythonic_assert_raises"]
33+
def isunexpandedtestmacro(tree):
34+
"""Return whether `tree` is an invocation of a testing macro, unexpanded."""
35+
return (type(tree) is Subscript and
36+
type(tree.value) is Name and
37+
tree.value.id in _test_macro_names)
38+
def isexpandedtestmacro(tree):
39+
"""Return whether `tree` is an invocation of a testing macro, expanded."""
40+
return (type(tree) is Call and
41+
any(isx(tree.func, fname, accept_attr=False)
42+
for fname in _test_function_names))
3343
def istestmacro(tree):
3444
"""Return whether `tree` is an invocation of a testing macro.
3545
36-
Expanded or unexpanded doesn't matter; this is currently provided
37-
so that other macros can detect and skip subtrees that invoke a test.
46+
Expanded or unexpanded doesn't matter.
3847
"""
39-
def isunexpandedtestmacro(tree):
40-
return (type(tree) is Subscript and
41-
type(tree.value) is Name and
42-
tree.value.id in _test_macro_names)
43-
def isexpandedtestmacro(tree):
44-
return (type(tree) is Call and
45-
any(isx(tree.func, fname, accept_attr=False)
46-
for fname in _test_function_names))
4748
return isunexpandedtestmacro(tree) or isexpandedtestmacro(tree)
4849

4950
# -----------------------------------------------------------------------------

0 commit comments

Comments
 (0)