From 49b772c9dc8cefbbea9b8817dd3cd724541c73df Mon Sep 17 00:00:00 2001 From: Juha Jeronen Date: Thu, 26 Sep 2024 14:14:10 +0300 Subject: [PATCH] fix Python 3.8 and 3.9 compatibility Walrus inside subscript (without parentheses) was added in Python 3.10. --- unpythonic/syntax/letdo.py | 9 +++- unpythonic/syntax/tests/test_letdo.py | 51 +++++++++++++---------- unpythonic/syntax/tests/test_letdoutil.py | 38 +++++++++-------- 3 files changed, 56 insertions(+), 42 deletions(-) diff --git a/unpythonic/syntax/letdo.py b/unpythonic/syntax/letdo.py index e2cb6240..5bda9cf7 100644 --- a/unpythonic/syntax/letdo.py +++ b/unpythonic/syntax/letdo.py @@ -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 @@ -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[] diff --git a/unpythonic/syntax/tests/test_letdo.py b/unpythonic/syntax/tests/test_letdo.py index 9415a604..3d94167c 100644 --- a/unpythonic/syntax/tests/test_letdo.py +++ b/unpythonic/syntax/tests/test_letdo.py @@ -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] @@ -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 @@ -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] @@ -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)"): @@ -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)] @@ -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)] @@ -486,7 +491,7 @@ 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. @@ -494,7 +499,7 @@ def 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. @@ -502,7 +507,7 @@ def 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(): diff --git a/unpythonic/syntax/tests/test_letdoutil.py b/unpythonic/syntax/tests/test_letdoutil.py index a5e46bb0..c0d82321 100644 --- a/unpythonic/syntax/tests/test_letdoutil.py +++ b/unpythonic/syntax/tests/test_letdoutil.py @@ -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. # -------------------------------------------------------------------------------- @@ -53,7 +57,7 @@ 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"): @@ -61,9 +65,9 @@ def validate(lst): 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` @@ -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 @@ -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 @@ -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)] @@ -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 @@ -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) @@ -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) @@ -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 @@ -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 @@ -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,