Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix statement pull-out in multi-item with #2606

Merged
merged 3 commits into from
Sep 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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