From 4a1cc7de50987cfa05905162f7aa5dba94c7c312 Mon Sep 17 00:00:00 2001 From: Kodi Arfer Date: Mon, 18 May 2026 17:25:18 -0400 Subject: [PATCH 1/2] Fix asynchronous comprehension forms --- NEWS.rst | 3 +- hy/core/result_macros.py | 51 ++++++++++++++++------------ tests/native_tests/comprehensions.hy | 16 +++++++++ 3 files changed, 47 insertions(+), 23 deletions(-) diff --git a/NEWS.rst b/NEWS.rst index 14b9bf457..d4a5a5bb0 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -19,7 +19,8 @@ New Features Bug Fixes ------------------------------ - +* Fixed a bug in which some asynchronous comprehension forms failed + to compile. * Fixed `_could_be_hy_src` in `importer.py` incorrectly returning `False` for valid Hy source paths that do not exist on disk, such as ``dfile`` override paths passed to `py_compile.compile`; Closes #2701. diff --git a/hy/core/result_macros.py b/hy/core/result_macros.py index 1cf09ff0d..b87975322 100644 --- a/hy/core/result_macros.py +++ b/hy/core/result_macros.py @@ -916,10 +916,11 @@ def compile_comprehension(compiler, expr, root, parts, final): # The desired comprehension can't be expressed as a # real Python comprehension. We'll write it as a nested # loop in a function instead. + any_async = False def f(parts): # This function is called recursively to construct # the nested loop. - nonlocal elt, ends_with_unpack + nonlocal elt, ends_with_unpack, any_async if not parts: if is_for: if body: @@ -958,6 +959,7 @@ def f(parts): if tagname in ("for", "afor"): orelse = orel and orel.pop().stmts node = asty.AsyncFor if tagname == "afor" else asty.For + any_async = any_async or tagname == "afor" return v[1] + node( v[1], target=v[0], @@ -1022,7 +1024,10 @@ def f(parts): expr, test=asty.Constant(expr, value=False), body=if_body, orelse=[] ) - ret += asty.FunctionDef( + body = f(parts).stmts + # `f` needs to be called before the next line so + # `any_async` is set early enough. + ret += (asty.AsyncFunctionDef if any_async else asty.FunctionDef)( expr, name=fname, args=ast.arguments( @@ -1034,31 +1039,33 @@ def f(parts): kw_defaults=[], defaults=[], ), - body=stmts + f(parts).stmts, + body=stmts + body, decorator_list=[], **({"type_params": []} if PY3_12 else {}), ) # Immediately call the new function. Unless the user asked - # for a generator, wrap the call in `[].__class__(...)` or - # `{}.__class__(...)` or `{1}.__class__(...)` to get the - # right type. We don't want to just use e.g. `list(...)` - # because the name `list` might be rebound. - return ret + Result( - expr=asty.parse( - expr, - "{}({}())".format( - { - asty.ListComp: "[].__class__", - asty.DictComp: "{}.__class__", - asty.SetComp: "{1}.__class__", - asty.GeneratorExp: "", - }[node_class], + # for a generator, wrap the call in another comprehension + # to get the right type. We don't want to just use e.g. + # `list(...)` because the name `list` might be rebound, and it + # doesn't work with async generators. + brackets = "[]" if node_class is asty.ListComp else "{}" + if node_class is asty.DictComp: + v1, v2 = compiler.get_anon_var(), compiler.get_anon_var() + v1, v2 = f"{v1}: {v2}", f"{v1}, {v2}" + else: + v1 = v2 = compiler.get_anon_var() + return ret + Result(expr = + asty.parse(expr, + f"{fname}()" + if node_class is asty.GeneratorExp else + "{}{} {} for {} in {}(){}".format( + brackets[0], + v1, + "async" if any_async else "", + v2, fname, - ), - ) - .body[0] - .value - ) + brackets[1])) + .body[0].value) # We can produce a real comprehension. generators = [] diff --git a/tests/native_tests/comprehensions.hy b/tests/native_tests/comprehensions.hy index 283958e99..8c036bfb2 100644 --- a/tests/native_tests/comprehensions.hy +++ b/tests/native_tests/comprehensions.hy @@ -183,6 +183,22 @@ (assert (= (next g) "last")))))) +(defn test-async-lfor [] + ; https://github.com/hylang/hy/issues/2712 + (do-mac (lfor statementize [False True] `(do + (defn :async top [] + (defn :async numbers [] + (for [i [0 1 2]] + (yield i))) + (setv g (lfor + :async i (numbers) + ~@(when statementize '[:do (print "hi")]) + (+ i 10))) + (print g) + g) + (assert (= (asyncio.run (top)) [10 11 12])))))) + + (defn test-raise-in-comp [] (defclass E [Exception] []) (setv l []) From 82a52fca0f349953bb09c3618090f6f3312584af Mon Sep 17 00:00:00 2001 From: Kodi Arfer Date: Mon, 18 May 2026 17:37:41 -0400 Subject: [PATCH 2/2] Update NEWS for the new release --- NEWS.rst | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/NEWS.rst b/NEWS.rst index d4a5a5bb0..c449e9fb9 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -2,36 +2,32 @@ Hy is `semantically versioned `__ since 1.0.0. -Unreleased +1.3.0 ("Dogs Should Be Raw", released 2026-05-24) ====================================================================== -Supports Python 3.x – Python 3.y +Supports Python 3.9 – Python 3.15 New Features ------------------------------ +* Added t-strings, and a pragma `bracketed-templates`. * Unpacking is now supported in comprehensions (PEP 798), even on Pythons < 3.15. -* ``(import :lazy …)`` is allowed on Pythons ≥ 3.15 (PEP 810). -* ``hyc`` now supports ``-q``/``--quiet`` to suppress progress messages. -* Added support for t-strings from Python 3.14. -* Added new pragma `bracketed-templates` to allow parsing `#[t[...]t]` and `#[t-[...]t-]` as template strings (analogous to bracketed f-strings). -* Added ``hy-repr``\s for ``Template`` and ``Interpolation``. +* `(import :lazy …)` is allowed on Pythons ≥ 3.15 (PEP 810). +* Added `hy-repr`\s for `Template`, `Interpolation`, and `frozendict`. +* `hyc` has a new command-line option `--quiet`. Bug Fixes ------------------------------ -* Fixed a bug in which some asynchronous comprehension forms failed - to compile. -* Fixed `_could_be_hy_src` in `importer.py` incorrectly returning - `False` for valid Hy source paths that do not exist on disk, such - as ``dfile`` override paths passed to `py_compile.compile`; Closes #2701. -* Captured exception variables are now properly scoped. +* Invalid conversion characters in f-strings now properly raise a + syntax error. +* Variables set by `(except …)` are now properly scoped. +* Fixed a crash when using `require` in `hy.eval`. +* Fixed some compilation failures for asynchronous comprehension forms. +* More model types (`String`, `Bytes`, `Symbol`, `Integer`, `Float`, + `Complex`) now work properly with `match`. +* Fixed a bug that could cause `py_compile.compile` to read Hy as Python. * Fixed bugs in `hy.model-patterns.whole` (and other combinators that use it, like `brackets`) in which tuples were not always produced. -* Added pattern matching support to String, Bytes, Symbol, Integer, Float, - and Complex models. -* Fixed an importlib exception when using `hy.eval` on code that - required other modules. -* Invalid conversion chars in f-strings now properly raise a syntax error. 1.2.0 ("Crackers and Snacks", released 2026-01-14) ======================================================================