diff --git a/NEWS.rst b/NEWS.rst index 4d6eacd08..33142287b 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -11,6 +11,8 @@ Bug Fixes ------------------------------ * Fixed a crash on Python 3.12.6. * Keyword objects can now be compared to each other with `<` etc. +* The order of evaluation in multi-item `with`\s now matches that of + nested one-item `with`\s. * Fixed a bug in which the REPL misinterpreted the symbol `pass`. 0.29.0 (released 2024-05-20) diff --git a/hy/core/result_macros.py b/hy/core/result_macros.py index 573e8167f..1636f177e 100644 --- a/hy/core/result_macros.py +++ b/hy/core/result_macros.py @@ -1116,11 +1116,12 @@ def compile_with_expression(compiler, expr, root, args, body): expr, targets=[name], value=asty.Constant(expr, value=None) ) + [args] = args ret = Result(stmts=[initial_assign]) items = [] was_async = None cbody = None - for i, (is_async, variable, ctx) in enumerate(args[0]): + for i, (is_async, variable, ctx) in enumerate(args): is_async = bool(is_async) if was_async is None: was_async = is_async @@ -1128,10 +1129,26 @@ def compile_with_expression(compiler, expr, root, args, body): # We're compiling a `with` that mixes synchronous and # asynchronous context managers. Python doesn't support # this directly, so start a new `with` inside the body. - cbody = compile_with_expression(compiler, expr, root, [args[0][i:]], body) + cbody = compile_with_expression(compiler, expr, root, [args[i:]], body) break - ctx = compiler.compile(ctx) - ret += ctx + if not isinstance(ctx, Result): + # In a non-recursive call, `ctx` has not yet been compiled, + # and thus is not yet a `Result`. + ctx = compiler.compile(ctx) + if i == 0: + ret += ctx + elif ctx.stmts: + # We need to include some statements as part of this + # context manager, but this `with` already has at + # least one prior context manager. So, put our + # statements in the body and then start a new `with`. + cbody = ctx + compile_with_expression( + compiler, + expr, + root, + [((is_async, variable, ctx), *args[i + 1:])], + body) + break variable = ( None if variable == Symbol("_") diff --git a/tests/native_tests/with.hy b/tests/native_tests/with.hy index 48f481715..94d26bce9 100644 --- a/tests/native_tests/with.hy +++ b/tests/native_tests/with.hy @@ -1,7 +1,8 @@ (import asyncio + unittest.mock [Mock] pytest - tests.resources [async-test AsyncWithTest]) + tests.resources [async-test AsyncWithTest async-exits]) (defn test-context [] (with [fd (open "tests/resources/text.txt" "r")] (assert fd)) @@ -12,36 +13,43 @@ (with [fd (open filename "r" :encoding "UTF-8")] (.read fd))) (assert (= (read-file "tests/resources/text.txt") "TAARGÜS TAARGÜS\n"))) +(setv exits []) (defclass WithTest [object] (defn __init__ [self val] - (setv self.val val) - None) + (setv self.val val)) (defn __enter__ [self] self.val) (defn __exit__ [self type value traceback] - (setv self.val None))) + (.append exits self.val))) (defn test-single-with [] + (setv (cut exits) []) (with [t (WithTest 1)] (setv out t)) - (assert (= out 1))) + (assert (= out 1)) + (assert (= exits [1]))) (defn test-quince-with [] + (setv (cut exits) []) (with [t1 (WithTest 1) t2 (WithTest 2) t3 (WithTest 3) _ (WithTest 4)] (setv out [t1 t2 t3])) - (assert (= out [1 2 3]))) + (assert (= out [1 2 3])) + (assert (= exits [4 3 2 1]))) (defn [async-test] test-single-with-async [] + (setv (cut async-exits) []) (setv out []) (asyncio.run ((fn :async [] (with [:async t (AsyncWithTest 1)] (.append out t))))) - (assert (= out [1]))) + (assert (= out [1])) + (assert (= async-exits [1]))) (defn [async-test] test-quince-with-async [] + (setv (cut async-exits) []) (setv out []) (asyncio.run ((fn :async [] @@ -51,9 +59,11 @@ :async t3 (AsyncWithTest 3) :async _ (AsyncWithTest 4)] (.extend out [t1 t2 t3]))))) - (assert (= out [1 2 3]))) + (assert (= out [1 2 3])) + (assert (= async-exits [4 3 2 1]))) (defn [async-test] test-with-mixed-async [] + (setv (cut exits) []) (setv out []) (asyncio.run ((fn :async [] @@ -97,3 +107,17 @@ (setv w (with [(SuppressZDE)] (.append l w) (/ 1 0) 5)) (assert (is w None)) (assert (= l [7]))) + +(defn test-statements [] + + (setv m (Mock)) + (with [t (do (m) (WithTest 2))] + (setv out t)) + (assert (= m.call-count 1)) + (assert (= out 2)) + + ; https://github.com/hylang/hy/issues/2605 + (with [t1 (WithTest 1) t2 (do (setv foo t1) (WithTest 2))] + (setv out [t1 t2])) + (assert (= out [1 2])) + (assert (= foo 1))) diff --git a/tests/resources/__init__.py b/tests/resources/__init__.py index 4ecbd1d9f..46142db78 100644 --- a/tests/resources/__init__.py +++ b/tests/resources/__init__.py @@ -19,6 +19,7 @@ def function_with_a_dash(): ) +async_exits = [] class AsyncWithTest: def __init__(self, val): self.val = val @@ -27,7 +28,7 @@ async def __aenter__(self): return self.val async def __aexit__(self, exc_type, exc, traceback): - self.val = None + async_exits.append(self.val) async def async_loop(items):