diff --git a/NEWS.rst b/NEWS.rst
index 14b9bf457..c449e9fb9 100644
--- a/NEWS.rst
+++ b/NEWS.rst
@@ -2,35 +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 `_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)
======================================================================
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 [])