Skip to content

Commit

Permalink
fix Python 3.8 and 3.9 compatibility
Browse files Browse the repository at this point in the history
Walrus inside subscript (without parentheses) was added in Python 3.10.
  • Loading branch information
Technologicat committed Sep 26, 2024
1 parent 26ed664 commit 49b772c
Show file tree
Hide file tree
Showing 3 changed files with 56 additions and 42 deletions.
9 changes: 7 additions & 2 deletions unpythonic/syntax/letdo.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ def dlet(tree, *, args, syntax, expander, **kw):
@dlet[x := 0]
def count():
(x := x + 1) # walrus requires parens here; or use `x << x + 1`
(x := x + 1)
return x
assert count() == 1
assert count() == 2
Expand Down Expand Up @@ -926,7 +926,12 @@ def _do0(tree):
raise SyntaxError("do0 body: expected a sequence of comma-separated expressions") # pragma: no cover
elts = tree.elts
# Use `local[]` and `do[]` as hygienically captured macros.
newelts = [q[a[_our_local][_do0_result := a[elts[0]]]], # noqa: F821, local[] defines it inside the do[].
#
# Python 3.8 and Python 3.9 require the parens around the walrus when used inside a subscript.
# TODO: Remove the parens when we bump minimum Python to 3.10.
# From https://docs.python.org/3/whatsnew/3.10.html:
# Assignment expressions can now be used unparenthesized within set literals and set comprehensions, as well as in sequence indexes (but not slices).
newelts = [q[a[_our_local][(_do0_result := a[elts[0]])]], # noqa: F821, local[] defines it inside the do[].
*elts[1:],
q[_do0_result]] # noqa: F821
return q[a[_our_do][t[newelts]]] # do0[] is also just a do[]
Expand Down
51 changes: 28 additions & 23 deletions unpythonic/syntax/tests/test_letdo.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,12 @@ def runtests():
# (including nested ``let`` constructs and similar).
# - No need for ``lambda e: ...`` wrappers. Inserted automatically,
# so the lines are only evaluated as the underlying seq.do() runs.
d1 = do[local[x := 17],
#
# Python 3.8 and Python 3.9 require the parens around the walrus when used inside a subscript.
# TODO: Remove the parens (in all walrus-inside-subscript instances in this file) when we bump minimum Python to 3.10.
# From https://docs.python.org/3/whatsnew/3.10.html:
# Assignment expressions can now be used unparenthesized within set literals and set comprehensions, as well as in sequence indexes (but not slices).
d1 = do[local[(x := 17)],
print(x),
x := 23,
x]
Expand All @@ -37,7 +42,7 @@ def runtests():

# v0.14.0: do[] now supports deleting previously defined local names with delete[]
a = 5
d = do[local[a := 17], # noqa: F841, yes, d is unused.
d = do[local[(a := 17)], # noqa: F841, yes, d is unused.
test[a == 17],
delete[a],
test[a == 5], # lexical scoping
Expand All @@ -46,7 +51,7 @@ def runtests():
test_raises[KeyError, do[delete[a], ], "should have complained about deleting nonexistent local 'a'"]

# do0[]: like do[], but return the value of the **first** expression
d2 = do0[local[y := 5], # noqa: F821, `local` defines the name on the LHS of the `<<`.
d2 = do0[local[(y := 5)], # noqa: F821, `local` defines the name on the LHS of the `<<`.
print("hi there, y =", y), # noqa: F821
42] # evaluated but not used
test[d2 == 5]
Expand Down Expand Up @@ -75,30 +80,30 @@ def runtests():
# Let macros. Lexical scoping supported.
with testset("let, letseq, letrec basic usage (new env-assignment syntax 0.15.3+)"):
# parallel binding, i.e. bindings don't see each other
test[let[x := 17,
y := 23][ # noqa: F821, `let` defines `y` here.
test[let[(x := 17),
(y := 23)][ # noqa: F821, `let` defines `y` here.
(x, y)] == (17, 23)] # noqa: F821

# sequential binding, i.e. Scheme/Racket let*
test[letseq[x := 1,
y := x + 1][ # noqa: F821
test[letseq[(x := 1),
(y := x + 1)][ # noqa: F821
(x, y)] == (1, 2)] # noqa: F821

test[letseq[x := 1,
x := x + 1][ # in a letseq, rebinding the same name is ok
test[letseq[(x := 1),
(x := x + 1)][ # in a letseq, rebinding the same name is ok
x] == 2]

# letrec sugars unpythonic.lispylet.letrec, removing the need for quotes on LHS
# and "lambda e: ..." wrappers on RHS (these are inserted by the macro):
test[letrec[evenp := (lambda x: (x == 0) or oddp(x - 1)), # noqa: F821, `letrec` defines `evenp` here.
oddp := (lambda x: (x != 0) and evenp(x - 1))][ # noqa: F821
test[letrec[(evenp := (lambda x: (x == 0) or oddp(x - 1))), # noqa: F821, `letrec` defines `evenp` here.
(oddp := (lambda x: (x != 0) and evenp(x - 1)))][ # noqa: F821
evenp(42)] is True] # noqa: F821

# nested letrecs work, too - each environment is internally named by a gensym
# so that outer ones "show through":
test[letrec[z := 9000][ # noqa: F821
letrec[evenp := (lambda x: (x == 0) or oddp(x - 1)), # noqa: F821
oddp := (lambda x: (x != 0) and evenp(x - 1))][ # noqa: F821
test[letrec[(z := 9000)][ # noqa: F821
letrec[(evenp := (lambda x: (x == 0) or oddp(x - 1))), # noqa: F821
(oddp := (lambda x: (x != 0) and evenp(x - 1)))][ # noqa: F821
(evenp(42), z)]] == (True, 9000)] # noqa: F821

with testset("let, letseq, letrec basic usage (previous modern env-assignment syntax)"):
Expand Down Expand Up @@ -151,8 +156,8 @@ def runtests():

# implicit do: an extra set of brackets denotes a multi-expr body
with testset("implicit do (extra bracket syntax for multi-expr let body) (new env-assignment syntax v0.15.3+)"):
a = let[x := 1,
y := 2][[ # noqa: F821
a = let[(x := 1),
(y := 2)][[ # noqa: F821
y := 1337, # noqa: F821
(x, y)]] # noqa: F821
test[a == (1, 1337)]
Expand All @@ -164,14 +169,14 @@ def runtests():
test[a == [1, 2]]

# implicit do works also in letseq, letrec
a = letseq[x := 1,
y := x + 1][[ # noqa: F821
a = letseq[(x := 1),
(y := x + 1)][[ # noqa: F821
x := 1337,
(x, y)]] # noqa: F821
test[a == (1337, 2)]

a = letrec[x := 1,
y := x + 1][[ # noqa: F821
a = letrec[(x := 1),
(y := x + 1)][[ # noqa: F821
x := 1337,
(x, y)]] # noqa: F821
test[a == (1337, 2)]
Expand Down Expand Up @@ -486,23 +491,23 @@ def test14():
x = "the nonlocal x" # restore the test environment

# v0.15.3+: walrus syntax
@dlet[x := "the env x"]
@dlet[(x := "the env x")]
def test15():
def inner():
(x := "updated env x") # noqa: F841, this writes to the let env since there is no `x` in an intervening scope, according to Python's standard rules.
inner()
return x
test[test15() == "updated env x"]

@dlet[x := "the env x"]
@dlet[(x := "the env x")]
def test16():
def inner():
x = "the inner x" # noqa: F841, unused on purpose, for testing. An assignment *statement* does NOT write to the let env.
inner()
return x
test[test16() == "the env x"]

@dlet[x := "the env x"]
@dlet[(x := "the env x")]
def test17():
x = "the local x" # This lexical variable shadows the env x.
def inner():
Expand Down
38 changes: 21 additions & 17 deletions unpythonic/syntax/tests/test_letdoutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,16 @@ def validate(lst):
if type(k) is not Name:
return False # pragma: no cover, only reached if the test fails.
return True
# Python 3.8 and Python 3.9 require the parens around the walrus when used inside a subscript.
# TODO: Remove the parens (in all walrus-inside-subscript instances in this file) when we bump minimum Python to 3.10.
# From https://docs.python.org/3/whatsnew/3.10.html:
# Assignment expressions can now be used unparenthesized within set literals and set comprehensions, as well as in sequence indexes (but not slices).
test[validate(the[canonize_bindings(q[k0, v0].elts)])] # noqa: F821, it's quoted.
test[validate(the[canonize_bindings(q[((k0, v0),)].elts)])] # noqa: F821
test[validate(the[canonize_bindings(q[(k0, v0), (k1, v1)].elts)])] # noqa: F821
test[validate(the[canonize_bindings([q[k0 := v0]])])] # noqa: F821, it's quoted.
test[validate(the[canonize_bindings([q[(k0 := v0)]])])] # noqa: F821, it's quoted.
test[validate(the[canonize_bindings([q[k0 << v0]])])] # noqa: F821, it's quoted.
test[validate(the[canonize_bindings(q[k0 := v0, k1 := v1].elts)])] # noqa: F821, it's quoted.
test[validate(the[canonize_bindings(q[(k0 := v0), (k1 := v1)].elts)])] # noqa: F821, it's quoted.
test[validate(the[canonize_bindings(q[k0 << v0, k1 << v1].elts)])] # noqa: F821, it's quoted.

# --------------------------------------------------------------------------------
Expand All @@ -53,17 +57,17 @@ def validate(lst):
# need this utility, so we must test it first.
with testset("isenvassign"):
test[not isenvassign(q[x])] # noqa: F821
test[isenvassign(q[x := 42])] # noqa: F821
test[isenvassign(q[(x := 42)])] # noqa: F821
test[isenvassign(q[x << 42])] # noqa: F821

with testset("islet"):
test[not islet(q[x])] # noqa: F821
test[not islet(q[f()])] # noqa: F821

# unpythonic 0.15.3+, Python 3.8+
test[islet(the[expandrq[let[x := 21][2 * x]]]) == ("expanded_expr", "let")] # noqa: F821, `let` defines `x`
test[islet(the[expandrq[let[(x := 21)][2 * x]]]) == ("expanded_expr", "let")] # noqa: F821, `let` defines `x`
test[islet(the[expandrq[let[[x := 21] in 2 * x]]]) == ("expanded_expr", "let")] # noqa: F821
test[islet(the[expandrq[let[2 * x, where[x := 21]]]]) == ("expanded_expr", "let")] # noqa: F821
test[islet(the[expandrq[let[2 * x, where[(x := 21)]]]]) == ("expanded_expr", "let")] # noqa: F821

# unpythonic 0.15.0 to 0.15.2, previous modern notation for bindings
test[islet(the[expandrq[let[x << 21][2 * x]]]) == ("expanded_expr", "let")] # noqa: F821, `let` defines `x`
Expand Down Expand Up @@ -96,7 +100,7 @@ def f2():
return 2 * x # noqa: F821
test[islet(the[testdata[0].decorator_list[0]]) == ("expanded_decorator", "let")]

testdata = q[let[x := 21][2 * x]] # noqa: F821
testdata = q[let[(x := 21)][2 * x]] # noqa: F821
test[islet(the[testdata], expanded=False) == ("lispy_expr", "let")]

testdata = q[let[x << 21][2 * x]] # noqa: F821
Expand Down Expand Up @@ -196,7 +200,7 @@ def f5():
test[not isdo(q[f()])] # noqa: F821

# unpythonic 0.15.3+, Python 3.8+
test[isdo(the[expandrq[do[x := 21, # noqa: F821
test[isdo(the[expandrq[do[(x := 21), # noqa: F821
2 * x]]]) == "expanded"] # noqa: F821

test[isdo(the[expandrq[do[x << 21, # noqa: F821
Expand All @@ -210,16 +214,16 @@ def f5():
test[isdo(the[thedo]) == "curried"]

# unpythonic 0.15.3+, Python 3.8+
testdata = q[do[x := 21, # noqa: F821
testdata = q[do[(x := 21), # noqa: F821
2 * x]] # noqa: F821
test[isdo(the[testdata], expanded=False) == "do"]

testdata = q[do0[23, # noqa: F821
x := 21, # noqa: F821
(x := 21), # noqa: F821
2 * x]] # noqa: F821
test[isdo(the[testdata], expanded=False) == "do0"]

testdata = q[someothermacro[x := 21, # noqa: F821
testdata = q[someothermacro[(x := 21), # noqa: F821
2 * x]] # noqa: F821
test[not isdo(the[testdata], expanded=False)]

Expand All @@ -241,7 +245,7 @@ def f5():
# Destructuring - envassign

with testset("envassign destructuring (new env-assign syntax v0.15.3+)"):
testdata = q[x := 42] # noqa: F821
testdata = q[(x := 42)] # noqa: F821
view = UnexpandedEnvAssignView(testdata)

# read
Expand Down Expand Up @@ -316,7 +320,7 @@ def testletdestructuring(testdata):
test[unparse(view.body) == "(z * t)"]

# lispy expr
testdata = q[let[x := 21, y := 2][y * x]] # noqa: F821
testdata = q[let[(x := 21), (y := 2)][y * x]] # noqa: F821
testletdestructuring(testdata)
testdata = q[let[x << 21, y << 2][y * x]] # noqa: F821
testletdestructuring(testdata)
Expand Down Expand Up @@ -374,7 +378,7 @@ def testletdestructuring(testdata):
testletdestructuring(testdata)

# disembodied haskelly let-where (just the content, no macro invocation)
testdata = q[y * x, where[x := 21, y := 2]] # noqa: F821
testdata = q[y * x, where[(x := 21), (y := 2)]] # noqa: F821
testletdestructuring(testdata)
testdata = q[y * x, where[x << 21, y << 2]] # noqa: F821
testletdestructuring(testdata)
Expand Down Expand Up @@ -599,7 +603,7 @@ def f8():
# Destructuring - unexpanded do

with testset("do destructuring (unexpanded) (new env-assign syntax v0.15.3+)"):
testdata = q[do[local[x := 21], # noqa: F821
testdata = q[do[local[(x := 21)], # noqa: F821
2 * x]] # noqa: F821
view = UnexpandedDoView(testdata)
# read
Expand All @@ -611,11 +615,11 @@ def f8():
test[isenvassign(the[thing])]
# write
# This mutates the original, but we have to assign `view.body` to trigger the setter.
thebody[0] = q[local[x := 9001]] # noqa: F821
thebody[0] = q[local[(x := 9001)]] # noqa: F821
view.body = thebody

# implicit do, a.k.a. extra bracket syntax
testdata = q[let[[local[x := 21], # noqa: F821
testdata = q[let[[local[(x := 21)], # noqa: F821
2 * x]]] # noqa: F821
if sys.version_info >= (3, 9, 0): # Python 3.9+: the Index wrapper is gone.
theimplicitdo = testdata.slice
Expand All @@ -630,7 +634,7 @@ def f8():
thing = thebody[0].slice.value
test[isenvassign(the[thing])]
# write
thebody[0] = q[local[x := 9001]] # noqa: F821
thebody[0] = q[local[(x := 9001)]] # noqa: F821
view.body = thebody

test_raises[TypeError,
Expand Down

0 comments on commit 49b772c

Please sign in to comment.