Skip to content

Commit

Permalink
Merge pull request #2606 from Kodiologist/with-pull-out
Browse files Browse the repository at this point in the history
Fix statement pull-out in multi-item `with`
  • Loading branch information
Kodiologist authored Sep 20, 2024
2 parents 9cbd040 + ae844e8 commit 3053e8b
Show file tree
Hide file tree
Showing 4 changed files with 57 additions and 13 deletions.
2 changes: 2 additions & 0 deletions NEWS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
25 changes: 21 additions & 4 deletions hy/core/result_macros.py
Original file line number Diff line number Diff line change
Expand Up @@ -1116,22 +1116,39 @@ 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
elif is_async != was_async:
# 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("_")
Expand Down
40 changes: 32 additions & 8 deletions tests/native_tests/with.hy
Original file line number Diff line number Diff line change
@@ -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))
Expand All @@ -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 []
Expand All @@ -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 []
Expand Down Expand Up @@ -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)))
3 changes: 2 additions & 1 deletion tests/resources/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ def function_with_a_dash():
)


async_exits = []
class AsyncWithTest:
def __init__(self, val):
self.val = val
Expand All @@ -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):
Expand Down

0 comments on commit 3053e8b

Please sign in to comment.