예제 #1
0
    def testexpandedletdestructuring(testdata):
        view = ExpandedLetView(testdata)

        # read
        # In the expanded form, the outer container of bindings is an `ast.Tuple`.
        test[len(view.bindings.elts) == 2]
        test[unparse(view.bindings.elts[0]) == "('x', 21)"]  # the variable names are strings
        test[unparse(view.bindings.elts[1]) == "('y', 2)"]

        # Reading an expanded let body is painful:
        lam = view.body  # lambda e: e.y * e.x
        test[type(the[lam]) is Lambda]
        lambody = lam.body
        test[type(the[lambody]) is BinOp]
        test[type(the[lambody.left]) is Attribute and lambody.left.attr == "y"]
        test[type(the[lambody.right]) is Attribute and lambody.right.attr == "x"]

        # write
        newbindings = q[("z", 21), ("t", 2)]  # noqa: F821
        view.bindings = newbindings
        test[len(view.bindings.elts) == 2]
        test[unparse(view.bindings.elts[0]) == "('z', 21)"]
        test[unparse(view.bindings.elts[1]) == "('t', 2)"]

        # edit an expanded let body
        envname = view.envname
        newbody = q[lambda _: name[envname].z * name[envname].t]  # noqa: F821
        view.body = newbody  # the body lambda gets the correct envname auto-injected as its arg

        lam = view.body  # lambda e: e.z * e.t
        test[type(lam) is Lambda]
        lambody = lam.body
        test[type(lambody) is BinOp]
        test[type(the[lambody.left]) is Attribute and lambody.left.attr == "z"]
        test[type(the[lambody.right]) is Attribute and lambody.right.attr == "t"]
예제 #2
0
def _test_expr_signals_or_raises(tree, syntaxname, asserter):
    ln = q[u[tree.lineno]] if hasattr(tree, "lineno") else q[None]
    filename = q[h[callsite_filename]()]

    # test_signals[exctype, expr, message]
    if type(tree) is Tuple and len(tree.elts) == 3:
        exctype, tree, message = tree.elts
    # test_signals[exctype, expr]
    elif type(tree) is Tuple and len(tree.elts) == 2:
        exctype, tree = tree.elts
        message = q[None]
    else:
        raise SyntaxError(
            f"Expected one of {syntaxname}[exctype, expr], {syntaxname}[exctype, expr, message]"
        )  # pragma: no cover

    # Same remark about outside-in source code capture as in `_test_expr`.
    sourcecode = unparse(tree)

    # Name our lambda to make the stack trace more understandable.
    # For consistency, the name matches that used by `_test_expr`.
    func_tree = q[h[namelambda]("testexpr")(lambda: a[tree])]
    return q[(a[asserter])(a[exctype],
                           u[sourcecode],
                           a[func_tree],
                           filename=a[filename],
                           lineno=a[ln],
                           message=a[message])]
예제 #3
0
 def canonize_expr(sourcecode):
     try:
         tree = parse(sourcecode)
     except SyntaxError:  # a repr might not be valid source code
         return None
     expr_node = tree.body[0]
     assert type(expr_node) is Expr
     return unparse(expr_node.value).strip()
예제 #4
0
def _test_expr(tree):
    # Note we want the line number *before macro expansion*, so we capture it now.
    ln = q[u[tree.lineno]] if hasattr(tree, "lineno") else q[None]
    filename = q[h[callsite_filename]()]
    asserter = q[h[unpythonic_assert]]

    # test[expr, message]  (like assert expr, message)
    if type(tree) is Tuple and len(tree.elts) == 2:
        tree, message = tree.elts
    # test[expr]  (like assert expr)
    else:
        message = q[None]

    # Before we edit the tree, get the source code in its pre-transformation
    # state, so we can include that into the test failure message.
    #
    # We capture the source in the outside-in pass, so that no macros inside `tree`
    # are expanded yet. For the same reason, we process the `the[]` marks in the
    # outside-in pass.
    #
    # (Note, however, that if the `test[]` is nested within the invocation of
    #  a code-walking block macro, that macro may have performed edits already.
    #  For this reason, we provide `with expand_testing_macros_first`, which
    #  in itself is a code-walking block macro, whose only purpose is to force
    #  `test[]` and its sisters to expand first.)
    sourcecode = unparse(tree)

    envname = gensym("e")  # for injecting the captured value

    # Handle the `the[...]` marks, if any.
    tree, the_exprs = _transform_important_subexpr(tree, envname=envname)
    if not the_exprs and type(
            tree) is Compare:  # inject the implicit the[] on the LHS
        tree.left = _inject_value_recorder(envname, tree.left)

    # We delay the execution of the test expr using a lambda, so
    # `unpythonic_assert` can get control first before the expr runs.
    #
    # Also, we need the lambda for passing in the value capture environment
    # for the `the[]` mark, anyway.
    #
    # We can't inject `lazy[]` here (to be more explicit this is a delay operation),
    # because we need to pass the environment.
    #
    # We name the lambda `testexpr` to make the stack trace more understandable.
    # If you change the name, change it also in `unpythonic_assert`.
    thelambda = q[lambda _: a[tree]]
    thelambda.args.args[0] = arg(
        arg=envname)  # inject the gensymmed parameter name
    func_tree = q[h[namelambda]("testexpr")(
        a[thelambda])]  # create the function that takes in the env

    return q[(a[asserter])(u[sourcecode],
                           a[func_tree],
                           filename=a[filename],
                           lineno=a[ln],
                           message=a[message])]
예제 #5
0
 def testbindings(*expected):
     for b, (k, v) in zip(view.bindings.elts, expected):
         test[len(b.elts) == 2]
         bk, lam = b.elts
         # outer quotes, source code; inner quotes, str within that source
         test[the[unparse(bk)] == the[f"'{k}'"]]
         test[type(the[lam]) is Lambda]
         lambody = lam.body
         test[type(the[lambody]) in (Constant, Num) and getconstant(lambody) == the[v]]  # Python 3.8: ast.Constant
예제 #6
0
def log(expr, **kw):
    """
    Print the passed value, labeling the output with the expression. Example:

        d = { 'a': 1 }
        log[d['a']]

        # prints
        # d['a']: 1
    """
    label = unparse(expr) + ': '
    return q[print(u[label], a[expr])]
예제 #7
0
def _dbg_expr(tree):
    # TODO: Do we really need to expand inside-out here?
    tree = dyn._macro_expander.visit_recursively(tree)

    ln = q[u[tree.lineno]] if hasattr(tree, "lineno") else q[None]
    filename = q[h[callsite_filename]()]
    # Careful here! We must `h[]` the `dyn`, but not `dbgprint_expr` itself,
    # because we want to look up that attribute dynamically.
    return q[h[dyn].dbgprint_expr(u[unparse(tree)],
                                  a[tree],
                                  filename=a[filename],
                                  lineno=a[ln])]
예제 #8
0
def log(expr, **kw):
    '''
    Print the passed value, labeling the output with the expression. Example:

        d = { 'a': 1 }
        log[d['a']]

        # prints
        # d['a']: 1
    '''
    label = unparse(expr) + ': '
    return Call(func=Name(id='print', ctx=Load()),
                args=[Str(s=label), expr],
                keywords=[])
예제 #9
0
 def transform(self, tree):
     if is_captured_value(tree):
         return tree  # don't recurse!
     if type(tree) is Call and type(
             tree.func) is Name and tree.func.id == pname:
         names = [q[u[unparse(node)]] for node in tree.args
                  ]  # x --> "x"; (1 + 2) --> "(1 + 2)"; ...
         names = q[t[names]]
         values = q[t[tree.args]]
         tree.args = [names, values]
         # can't use inspect.stack in the printer itself because we want the line number *before macro expansion*.
         lineno = tree.lineno if hasattr(tree, "lineno") else None
         tree.keywords += [
             keyword(arg="filename", value=q[h[callsite_filename]()]),
             keyword(arg="lineno", value=q[u[lineno]])
         ]
         tree.func = pfunc
     return self.generic_visit(tree)
예제 #10
0
def _test_block_signals_or_raises(block_body, args, syntaxname, asserter):
    if not block_body:
        return []  # pragma: no cover, cannot happen through the public API.
    first_stmt = block_body[0]

    # Note we want the line number *before macro expansion*, so we capture it now.
    ln = q[u[first_stmt.lineno]] if hasattr(first_stmt, "lineno") else q[None]
    filename = q[h[callsite_filename]()]

    # with test_raises[exctype, message]:
    if len(args) == 2:
        exctype, message = args
    # with test_raises[exctype]:
    elif len(args) == 1:
        exctype = args[0]
        message = q[None]
    else:
        raise SyntaxError(
            f'Expected `with {syntaxname}(exctype):` or `with {syntaxname}[exctype, message]:`'
        )  # pragma: no cover

    # Same remark about outside-in source code capture as in `_test_expr`.
    sourcecode = unparse(block_body)

    testblock_function_name = gensym("_test_block")
    thetest = q[(a[asserter])(a[exctype],
                              u[sourcecode],
                              n[testblock_function_name],
                              filename=a[filename],
                              lineno=a[ln],
                              message=a[message])]
    with q as newbody:

        def _insert_funcname_here_(
        ):  # no env needed, since `the[]` is not meaningful here.
            ...

        a[thetest]
    thefunc = newbody[0]
    thefunc.name = testblock_function_name
    thefunc.body = block_body
    return newbody
예제 #11
0
        def testletdestructuring(testdata):
            view = UnexpandedLetView(testdata)

            # read
            # In the unexpanded form, the outer container of bindings is a `list`.
            test[len(view.bindings) == 2]
            test[unparse(view.bindings[0]) == "(x, 21)"]  # the variable names are identifiers
            test[unparse(view.bindings[1]) == "(y, 2)"]
            test[unparse(view.body) == "(y * x)"]

            # write
            #
            # It's also legal to edit the AST nodes in view.bindings directly.
            # But the job of the setter, which we want to test here,
            # is to handle reassigning `view.bindings`.
            newbindings = q[(z, 21), (t, 2)].elts  # noqa: F821
            view.bindings = newbindings  # ...like this.
            view.body = q[z * t]  # noqa: F821
            test[len(view.bindings) == 2]
            test[unparse(view.bindings[0]) == "(z, 21)"]
            test[unparse(view.bindings[1]) == "(t, 2)"]
            test[unparse(view.body) == "(z * t)"]
예제 #12
0
def _inject_value_recorder(envname, tree):  # wrap tree with the the[] handler
    recorder = q[h[_record_value]]  # TODO: stash hygienic value?
    return q[a[recorder](n[envname], u[unparse(tree)], a[tree])]
예제 #13
0
def _test_block(block_body, args):
    if not block_body:
        return []  # pragma: no cover, cannot happen through the public API.
    first_stmt = block_body[0]

    # Note we want the line number *before macro expansion*, so we capture it now.
    ln = q[u[first_stmt.lineno]] if hasattr(first_stmt, "lineno") else q[None]
    filename = q[h[callsite_filename]()]
    asserter = q[h[unpythonic_assert]]

    # with test[message]:
    if len(args) == 1:
        message = args[0]
    # with test:
    elif len(args) == 0:
        message = q[None]
    else:
        raise SyntaxError('Expected `with test:` or `with test[message]:`'
                          )  # pragma: no cover

    # Same remark about outside-in source code capture as in `_test_expr`.
    sourcecode = unparse(block_body)

    envname = gensym("e")  # for injecting the captured value

    # Handle the `the[...]` marks, if any.
    block_body, the_exprs = _transform_important_subexpr(block_body,
                                                         envname=envname)

    # Prepare the function template to be injected, and splice the contents
    # of the `with test` block as the function body.
    testblock_function_name = gensym("_test_block")
    thetest = q[(a[asserter])(u[sourcecode],
                              n[testblock_function_name],
                              filename=a[filename],
                              lineno=a[ln],
                              message=a[message])]
    with q as newbody:

        def _insert_funcname_here_(_insert_envname_here_):
            ...  # to be filled in below

        a[thetest]  # call the asserter
    thefunc = newbody[0]
    thefunc.name = testblock_function_name
    thefunc.args.args[0] = arg(
        arg=envname)  # inject the gensymmed parameter name
    thefunc.body = block_body

    # Handle the return statement.
    #
    # We just check if there is at least one; if so, we don't need to do
    # anything; the returned value is what the test should return to the
    # asserter.
    for stmt in thefunc.body:
        if type(stmt) is Return:
            retval = stmt.value
            if not the_exprs and type(retval) is Compare:
                # inject the implicit the[] on the LHS
                retval.left = _inject_value_recorder(envname, retval.left)
            break
    else:
        # When there is no return statement at the top level of the `with test` block,
        # we inject a `return True` to satisfy the test when the injected function
        # returns normally.
        with q as thereturn:
            return True
        thefunc.body.extend(thereturn)

    return newbody
예제 #14
0
def runtests():
    # --------------------------------------------------------------------------------
    # Internal-ish utilities

    with testset("canonize_bindings"):
        # canonize_bindings takes in a list of bindings, and outputs a list of bindings.
        def validate(lst):
            for b in lst:
                if type(b) is not Tuple or len(b.elts) != 2:
                    return False  # pragma: no cover, only reached if the test fails.
                k, v = b.elts
                if type(k) is not Name:
                    return False  # pragma: no cover, only reached if the test fails.
            return True
        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, k1 << v1].elts)])]  # noqa: F821, it's quoted.

    # --------------------------------------------------------------------------------
    # AST structure matching

    # The let[] and do[] macros, used in the tests of islet() and isdo(),
    # 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

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

        # modern notation for bindings
        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

        # classic notation for bindings
        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

        with expandrq as testdata:
            @dlet(x << 21)  # noqa: F821
            def f1():
                return 2 * x  # noqa: F821
        test[islet(the[testdata[0].decorator_list[0]]) == ("expanded_decorator", "let")]

        with expandrq as testdata:
            @dlet((x, 21))  # noqa: F821
            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
        test[islet(the[testdata], expanded=False) == ("lispy_expr", "let")]

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

        # one binding special case for haskelly let-in
        testdata = q[let[[x, 21] in 2 * x]]  # noqa: F821
        test[islet(the[testdata], expanded=False) == ("in_expr", "let")]
        testdata = q[let[(x, 21) in 2 * x]]  # noqa: F821
        test[islet(the[testdata], expanded=False) == ("in_expr", "let")]
        testdata = q[let[2 * x, where[x, 21]]]  # noqa: F821
        test[islet(the[testdata], expanded=False) == ("where_expr", "let")]
        testdata = q[let[2 * x, where(x, 21)]]  # noqa: F821
        test[islet(the[testdata], expanded=False) == ("where_expr", "let")]

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

        # some other macro invocation
        test[not islet(the[q[someothermacro((x, 21))[2 * x]]], expanded=False)]  # noqa: F821
        test[not islet(the[q[someothermacro[(x, 21) in 2 * x]]], expanded=False)]  # noqa: F821

        # invalid syntax for haskelly let-in (no delimiters around bindings subform)
        testdata = q[let[a in b]]  # noqa: F821
        test[not islet(the[testdata], expanded=False)]

        with q as testdata:
            @dlet((x, 21))  # noqa: F821
            def f3():
                return 2 * x  # noqa: F821
        test[islet(the[testdata[0].decorator_list[0]], expanded=False) == ("decorator", "dlet")]

        with q as testdata:
            @dlet(x << 21)  # noqa: F821
            def f4():
                return 2 * x  # noqa: F821
        test[islet(the[testdata[0].decorator_list[0]], expanded=False) == ("decorator", "dlet")]

    with testset("islet integration with autocurry"):
        # NOTE: We have to be careful with how we set up the test data here.
        #
        # The quasiquote operator must be outside the `with autocurry` block,
        # because otherwise `autocurry` will attempt to curry the AST-lifted
        # representation, leading to arguably funny but nonsensical things like
        # `ctx=currycall(ast.Load)`.
        with expandrq as testdata:
            with autocurry:
                let[x << 21][2 * x]  # noqa: F821  # note this goes into an ast.Expr
        thelet = testdata[0].value
        test[islet(the[thelet]) == ("curried_expr", "let")]

        with expandrq as testdata:
            with autocurry:
                let[[x << 21] in 2 * x]  # noqa: F821
        thelet = testdata[0].value
        test[islet(the[thelet]) == ("curried_expr", "let")]

        with expandrq as testdata:
            with autocurry:
                let[2 * x, where[x << 21]]  # noqa: F821
        thelet = testdata[0].value
        test[islet(the[thelet]) == ("curried_expr", "let")]

        with expandrq as testdata:
            with autocurry:
                let((x, 21))[2 * x]  # noqa: F821  # note this goes into an ast.Expr
        thelet = testdata[0].value
        test[islet(the[thelet]) == ("curried_expr", "let")]

        with expandrq as testdata:
            with autocurry:
                let[(x, 21) in 2 * x]  # noqa: F821
        thelet = testdata[0].value
        test[islet(the[thelet]) == ("curried_expr", "let")]

        with expandrq as testdata:
            with autocurry:
                let[2 * x, where(x, 21)]  # noqa: F821
        thelet = testdata[0].value
        test[islet(the[thelet]) == ("curried_expr", "let")]

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

        test[isdo(the[expandrq[do[x << 21,  # noqa: F821
                                  2 * x]]]) == "expanded"]  # noqa: F821

        with expandrq as testdata:
            with autocurry:
                do[x << 21,  # noqa: F821
                   2 * x]  # noqa: F821
        thedo = testdata[0].value
        test[isdo(the[thedo]) == "curried"]

        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
                         2 * x]]  # noqa: F821
        test[isdo(the[testdata], expanded=False) == "do0"]

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

    # --------------------------------------------------------------------------------
    # Destructuring - envassign

    with testset("envassign destructuring"):
        testdata = q[x << 42]  # noqa: F821
        view = UnexpandedEnvAssignView(testdata)

        # read
        test[view.name == "x"]
        test[type(the[view.value]) in (Constant, Num) and getconstant(view.value) == 42]  # Python 3.8: ast.Constant

        # write
        view.name = "y"
        view.value = q[23]
        test[view.name == "y"]
        test[type(the[view.value]) in (Constant, Num) and getconstant(view.value) == 23]  # Python 3.8: ast.Constant

        # it's a live view
        test[unparse(testdata) == "(y << 23)"]

        # error cases
        test_raises[TypeError,
                    UnexpandedEnvAssignView(q[x]),  # noqa: F821
                    "not an env assignment"]
        with test_raises[TypeError, "name must be str"]:
            view.name = 1234

    # --------------------------------------------------------------------------------
    # Destructuring - unexpanded let - the phase where all sensible people do their AST edits

    with testset("let destructuring (unexpanded)"):
        def testletdestructuring(testdata):
            view = UnexpandedLetView(testdata)

            # read
            # In the unexpanded form, the outer container of bindings is a `list`.
            test[len(view.bindings) == 2]
            test[unparse(view.bindings[0]) == "(x, 21)"]  # the variable names are identifiers
            test[unparse(view.bindings[1]) == "(y, 2)"]
            test[unparse(view.body) == "(y * x)"]

            # write
            #
            # It's also legal to edit the AST nodes in view.bindings directly.
            # But the job of the setter, which we want to test here,
            # is to handle reassigning `view.bindings`.
            newbindings = q[(z, 21), (t, 2)].elts  # noqa: F821
            view.bindings = newbindings  # ...like this.
            view.body = q[z * t]  # noqa: F821
            test[len(view.bindings) == 2]
            test[unparse(view.bindings[0]) == "(z, 21)"]
            test[unparse(view.bindings[1]) == "(t, 2)"]
            test[unparse(view.body) == "(z * t)"]

        # lispy expr
        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)
        testdata = q[let[(x, 21), (y, 2)][y * x]]  # noqa: F821
        testletdestructuring(testdata)

        # haskelly let-in
        testdata = q[let[[x << 21, y << 2] in y * x]]  # noqa: F821
        testletdestructuring(testdata)
        testdata = q[let[(x << 21, y << 2) in y * x]]  # noqa: F821
        testletdestructuring(testdata)
        testdata = q[let[[[x, 21], [y, 2]] in y * x]]  # noqa: F821
        testletdestructuring(testdata)
        testdata = q[let[[(x, 21), (y, 2)] in y * x]]  # noqa: F821
        testletdestructuring(testdata)
        testdata = q[let[([x, 21], [y, 2]) in y * x]]  # noqa: F821
        testletdestructuring(testdata)
        testdata = q[let[((x, 21), (y, 2)) in y * x]]  # noqa: F821
        testletdestructuring(testdata)

        # haskelly let-where
        testdata = q[let[y * x, where[x << 21, y << 2]]]  # noqa: F821
        testletdestructuring(testdata)
        testdata = q[let[y * x, where(x << 21, y << 2)]]  # noqa: F821
        testletdestructuring(testdata)
        testdata = q[let[y * x, where[[x, 21], [y, 2]]]]  # noqa: F821
        testletdestructuring(testdata)
        testdata = q[let[y * x, where[(x, 21), (y, 2)]]]  # noqa: F821
        testletdestructuring(testdata)
        testdata = q[let[y * x, where([x, 21], [y, 2])]]  # noqa: F821
        testletdestructuring(testdata)
        testdata = q[let[y * x, where((x, 21), (y, 2))]]  # noqa: F821
        testletdestructuring(testdata)

        # disembodied haskelly let-in (just the content, no macro invocation)
        testdata = q[[x << 21, y << 2] in y * x]  # noqa: F821
        testletdestructuring(testdata)
        testdata = q[(x << 21, y << 2) in y * x]  # noqa: F821
        testletdestructuring(testdata)
        testdata = q[[[x, 21], [y, 2]] in y * x]  # noqa: F821
        testletdestructuring(testdata)
        testdata = q[[(x, 21), (y, 2)] in y * x]  # noqa: F821
        testletdestructuring(testdata)
        testdata = q[([x, 21], [y, 2]) in y * x]  # noqa: F821
        testletdestructuring(testdata)
        testdata = q[((x, 21), (y, 2)) in y * x]  # noqa: F821
        testletdestructuring(testdata)

        # disembodied haskelly let-where (just the content, no macro invocation)
        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)
        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)
        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)

        # decorator
        with q as testdata:
            @dlet((x, 21), (y, 2))  # noqa: F821
            def f5():
                return 2 * x  # noqa: F821

        # read
        view = UnexpandedLetView(testdata[0].decorator_list[0])
        test[len(view.bindings) == 2]
        test[unparse(view.bindings[0]) == "(x, 21)"]
        test[unparse(view.bindings[1]) == "(y, 2)"]
        test_raises[TypeError,
                    view.body,
                    "decorator let does not have an accessible body"]

        # write
        newbindings = q[(z, 21), (t, 2)].elts  # noqa: F821
        view.bindings = newbindings
        test[len(view.bindings) == 2]
        test[unparse(view.bindings[0]) == "(z, 21)"]
        test[unparse(view.bindings[1]) == "(t, 2)"]
        with test_raises[TypeError, "decorator let does not have an accessible body"]:
            view.body = q[x]  # noqa: F821

        test_raises[TypeError,
                    UnexpandedLetView(q[x]),  # noqa: F821
                    "not a let form"]

    # --------------------------------------------------------------------------------
    # Destructuring - expanded let - for those unfortunate macros that expand after let[]
    # (yet need to edit its AST for interop purposes)

    def testexpandedletdestructuring(testdata):
        view = ExpandedLetView(testdata)

        # read
        # In the expanded form, the outer container of bindings is an `ast.Tuple`.
        test[len(view.bindings.elts) == 2]
        test[unparse(view.bindings.elts[0]) == "('x', 21)"]  # the variable names are strings
        test[unparse(view.bindings.elts[1]) == "('y', 2)"]

        # Reading an expanded let body is painful:
        lam = view.body  # lambda e: e.y * e.x
        test[type(the[lam]) is Lambda]
        lambody = lam.body
        test[type(the[lambody]) is BinOp]
        test[type(the[lambody.left]) is Attribute and lambody.left.attr == "y"]
        test[type(the[lambody.right]) is Attribute and lambody.right.attr == "x"]

        # write
        newbindings = q[("z", 21), ("t", 2)]  # noqa: F821
        view.bindings = newbindings
        test[len(view.bindings.elts) == 2]
        test[unparse(view.bindings.elts[0]) == "('z', 21)"]
        test[unparse(view.bindings.elts[1]) == "('t', 2)"]

        # edit an expanded let body
        envname = view.envname
        newbody = q[lambda _: name[envname].z * name[envname].t]  # noqa: F821
        view.body = newbody  # the body lambda gets the correct envname auto-injected as its arg

        lam = view.body  # lambda e: e.z * e.t
        test[type(lam) is Lambda]
        lambody = lam.body
        test[type(lambody) is BinOp]
        test[type(the[lambody.left]) is Attribute and lambody.left.attr == "z"]
        test[type(the[lambody.right]) is Attribute and lambody.right.attr == "t"]

    with testset("let destructuring (expanded let)"):
        # lispy expr
        testdata = expandrq[let[(x, 21), (y, 2)][y * x]]  # noqa: F821
        testexpandedletdestructuring(testdata)

        # haskelly let-in
        testdata = expandrq[let[((x, 21), (y, 2)) in y * x]]  # noqa: F821
        testexpandedletdestructuring(testdata)

        # haskelly let-where
        testdata = expandrq[let[y * x, where((x, 21), (y, 2))]]  # noqa: F821
        testexpandedletdestructuring(testdata)

        # decorator
        with expandrq as testdata:
            @dlet((x, 21), (y, 2))  # noqa: F821
            def f6():
                return 2 * x  # noqa: F821
        view = ExpandedLetView(testdata[0].decorator_list[0])
        test_raises[TypeError,
                    view.body,
                    "decorator let does not have an accessible body"]
        with test_raises[TypeError, "decorator let does not have an accessible body"]:
            view.body = q[x]  # noqa: F821
        test[view.envname is None]  # dlet decorator doesn't have an envname, either

        # let with implicit do (extra bracket syntax)
        #
        # with step_expansion:
        #     let[((x, 21)) in [local[z << 2],
        #                       z * x]]
        #
        # After macro expansion:
        #   letter((('x', 21),),
        #          namelambda('let_body')((lambda e1:
        #              dof(namelambda('do_line1')((lambda e: e._set('z', 2))),
        #                  namelambda('do_line2')((lambda e: (e.z * e1.x)))))),
        #          mode='let')
        #
        testdata = expandrq[let[((x, 21)) in [local[z << 2],  # noqa: F821
                                              z * x]]]  # noqa: F821
        view = ExpandedLetView(testdata)
        lam = view.body
        test[isdo(the[lam.body])]

        test_raises[TypeError,
                    ExpandedLetView(q[x]),  # noqa: F821
                    "not an expanded let form"]

    # --------------------------------------------------------------------------------
    # Destructuring - expanded letrec - for the truly desperate

    def testexpandedletrecdestructuring(testdata):
        view = ExpandedLetView(testdata)

        # With an expanded letrec, even reading the bindings gets painful:
        def testbindings(*expected):
            for b, (k, v) in zip(view.bindings.elts, expected):
                test[len(b.elts) == 2]
                bk, lam = b.elts
                # outer quotes, source code; inner quotes, str within that source
                test[the[unparse(bk)] == the[f"'{k}'"]]
                test[type(the[lam]) is Lambda]
                lambody = lam.body
                test[type(the[lambody]) in (Constant, Num) and getconstant(lambody) == the[v]]  # Python 3.8: ast.Constant

        # read
        test[len(view.bindings.elts) == 2]
        testbindings(('x', 21), ('y', 2))

        # Reading an expanded letrec body
        lam = view.body  # lambda e: e.y * e.x
        test[type(the[lam]) is Lambda]
        lambody = lam.body
        test[type(the[lambody]) is BinOp]
        test[type(the[lambody.left]) is Attribute and lambody.left.attr == "y"]
        test[type(the[lambody.right]) is Attribute and lambody.right.attr == "x"]

        # write
        newbindings = q[("z", lambda _: 21), ("t", lambda _: 2)]  # noqa: F821
        view.bindings = newbindings  # each binding lambda gets the correct envname auto-injected as its arg
        test[len(view.bindings.elts) == 2]
        testbindings(('z', 21), ('t', 2))

        # Editing an expanded letrec body
        envname = view.envname
        newbody = q[lambda _: name[envname].z * name[envname].t]  # noqa: F821
        view.body = newbody  # the body lambda gets the correct envname auto-injected as its arg

        lam = view.body  # lambda e: e.z * e.t
        test[type(lam) is Lambda]
        lambody = lam.body
        test[type(lambody) is BinOp]
        test[type(the[lambody.left]) is Attribute and lambody.left.attr == "z"]
        test[type(the[lambody.right]) is Attribute and lambody.right.attr == "t"]

    with testset("let destructuring (expanded letrec)"):
        # lispy expr
        testdata = expandrq[letrec[((x, 21), (y, 2)) in y * x]]  # noqa: F821
        testexpandedletrecdestructuring(testdata)

        # haskelly let-in
        testdata = expandrq[letrec[((x, 21), (y, 2)) in y * x]]  # noqa: F821
        testexpandedletrecdestructuring(testdata)

        # haskelly let-where
        testdata = expandrq[letrec[y * x, where((x, 21), (y, 2))]]  # noqa: F821
        testexpandedletrecdestructuring(testdata)

        # decorator, letrec
        with expandrq as testdata:
            @dletrec((x, 21), (y, 2))  # noqa: F821
            def f7():
                return 2 * x  # noqa: F821
        view = ExpandedLetView(testdata[0].decorator_list[0])
        test_raises[TypeError,
                    view.body,
                    "decorator let does not have an accessible body"]
        with test_raises[TypeError, "decorator let does not have an accessible body"]:
            view.body = q[x]  # noqa: F821
        test[view.envname is not None]  # dletrec decorator has envname in the bindings

    # --------------------------------------------------------------------------------
    # Destructuring - expanded let and letrec, integration with autocurry

    with testset("let destructuring (expanded) integration with autocurry"):
        with expandrq as testdata:
            with autocurry:
                let[((x, 21), (y, 2)) in y * x]  # noqa: F821  # note this goes into an ast.Expr
        thelet = testdata[0].value
        testexpandedletdestructuring(thelet)

        with expandrq as testdata:
            with autocurry:
                letrec[((x, 21), (y, 2)) in y * x]  # noqa: F821
        thelet = testdata[0].value
        testexpandedletrecdestructuring(thelet)

    # --------------------------------------------------------------------------------
    # Destructuring - unexpanded do

    with testset("do destructuring (unexpanded)"):
        testdata = q[do[local[x << 21],  # noqa: F821
                        2 * x]]  # noqa: F821
        view = UnexpandedDoView(testdata)
        # read
        thebody = view.body
        if sys.version_info >= (3, 9, 0):  # Python 3.9+: the Index wrapper is gone.
            thing = thebody[0].slice
        else:
            thing = thebody[0].slice.value
        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
        view.body = thebody

        # implicit do, a.k.a. extra bracket syntax
        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
        else:
            theimplicitdo = testdata.slice.value
        view = UnexpandedDoView(theimplicitdo)
        # read
        thebody = view.body
        if sys.version_info >= (3, 9, 0):  # Python 3.9+: the Index wrapper is gone.
            thing = thebody[0].slice
        else:
            thing = thebody[0].slice.value
        test[isenvassign(the[thing])]
        # write
        thebody[0] = q[local[x << 9001]]  # noqa: F821
        view.body = thebody

        test_raises[TypeError,
                    UnexpandedDoView(q[x]),  # noqa: F821
                    "not a do form"]

    # --------------------------------------------------------------------------------
    # Destructuring - expanded do

    with testset("do destructuring (expanded)"):
        testdata = expandrq[do[local[x << 21],  # noqa: F821
                               2 * x]]  # noqa: F821
        view = ExpandedDoView(testdata)
        test[view.envname is not None]

        # read
        # e._set('x', 21)
        thebody = view.body
        lam = thebody[0]
        test[type(the[lam.body]) is Call and
             type(lam.body.func) is Attribute and
             lam.body.func.attr == "_set" and
             unparse(lam.body.args[0]) == "'x'"]
        # write
        # This mutates the original, but we have to assign `view.body` to trigger the setter.
        envname = view.envname
        thebody[0] = q[lambda _: name[envname]._set('x', 9001)]  # noqa: F821
        view.body = thebody  # the body lambdas gets the correct envname auto-injected as their arg

        test_raises[TypeError,
                    ExpandedDoView(q[x]),  # noqa: F821
                    "not an expanded do form"]

    with testset("do destructuring (expanded) integration with autocurry"):
        with expandrq as testdata:
            with autocurry:
                do[local[x << 21],  # noqa: F821
                   2 * x]  # noqa: F821
        thedo = testdata[0].value
        view = ExpandedDoView(thedo)
        test[view.envname is not None]

        # read
        # currycall(e._set, 'x', 21)
        thebody = view.body
        lam = thebody[0]
        test[type(the[lam.body]) is Call and
             isx(lam.body.func, "currycall") and
             type(lam.body.args[0]) is Attribute and
             lam.body.args[0].attr == "_set" and
             unparse(lam.body.args[1]) == "'x'"]
        # write
        # This mutates the original, but we have to assign `view.body` to trigger the setter.
        envname = view.envname
        thebody[0] = q[lambda _: name[envname]._set('x', 9001)]  # noqa: F821
        view.body = thebody  # the body lambdas gets the correct envname auto-injected as their arg