diff --git a/NEWS.rst b/NEWS.rst index 603eea0ed..4f83389c5 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -12,6 +12,9 @@ New Features Bug Fixes ------------------------------ * Fixed a crash from using an empty string in a `(. …)` expression. +* `(except [[]] …)` now catches no exceptions, rather than being treated like + `(except [] …)`, which catches all exceptions. +* `(except [e []] …)` is now translated to Python correctly by `hy2py`. 1.0.0 ("Afternoon Review", released 2024-09-22) ====================================================================== diff --git a/docs/api.rst b/docs/api.rst index 867786ff1..cd32bfe4b 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -912,6 +912,8 @@ Exception-handling ``except ETYPE as VAR:`` - ``[VAR [ETYPE1 ETYPE2 …]]`` to catch any of the named types and bind it to ``VAR``, like Python's ``except ETYPE1, ETYPE2, … as VAR:`` + - ``[[]]`` or ``[VAR []]`` to catch no exceptions, like Python's + ``except ():``. The return value of ``try`` is the last form evaluated among the main body, ``except`` forms, ``except*`` forms, and ``else``. diff --git a/hy/core/result_macros.py b/hy/core/result_macros.py index 0b308037c..46d6b4b16 100644 --- a/hy/core/result_macros.py +++ b/hy/core/result_macros.py @@ -1449,20 +1449,24 @@ def compile_try_expression(compiler, expr, root, body, catchers, orelse, finalbo ) name = None - if len(exceptions) == 2: - name = mangle(compiler._nonconst(exceptions[0])) - - exceptions_list = exceptions[-1] if exceptions else List() - if isinstance(exceptions_list, List): - if len(exceptions_list): - # [FooBar BarFoo] → catch Foobar and BarFoo exceptions - elts, types, _ = compiler._compile_collect(exceptions_list) - types += asty.Tuple(exceptions_list, elts=elts, ctx=ast.Load()) - else: - # [] → all exceptions caught - types = Result() + if len(exceptions) == 0: + exceptions = "ALL" + elif len(exceptions) == 1: + [exceptions] = exceptions + else: + [name, exceptions] = exceptions + name = mangle(compiler._nonconst(name)) + + if exceptions == "ALL": + # Catch all exceptions. + types = Result() + elif isinstance(exceptions, List): + # [FooBar BarFoo] → Catch Foobar and BarFoo exceptions. + elts, types, _ = compiler._compile_collect(exceptions) + types += asty.Tuple(exceptions, elts=elts, ctx=ast.Load()) else: - types = compiler.compile(exceptions_list) + # A single exception type. + types = compiler.compile(exceptions) # Create a "fake" scope for the exception variable. # See: https://docs.python.org/3/reference/compound_stmts.html#the-try-statement diff --git a/tests/native_tests/try.hy b/tests/native_tests/try.hy index 2403a976c..9d490d820 100644 --- a/tests/native_tests/try.hy +++ b/tests/native_tests/try.hy @@ -70,6 +70,10 @@ (setv out ["ccc" (type e)])) (except [e [KeyError AttributeError]] (setv out ["ddd" (type e)])) + (except [[]] + (assert False)) + (except [e []] + (assert False)) (except [] (setv out ["eee" None])) (else diff --git a/tests/native_tests/with.hy b/tests/native_tests/with.hy index 94d26bce9..e88c59f51 100644 --- a/tests/native_tests/with.hy +++ b/tests/native_tests/with.hy @@ -2,7 +2,8 @@ asyncio unittest.mock [Mock] pytest - tests.resources [async-test AsyncWithTest async-exits]) + tests.resources [async-test] + tests.resources.pydemo [AsyncWithTest async-exits]) (defn test-context [] (with [fd (open "tests/resources/text.txt" "r")] (assert fd)) @@ -39,7 +40,7 @@ (assert (= exits [4 3 2 1]))) (defn [async-test] test-single-with-async [] - (setv (cut async-exits) []) + (.clear async-exits) (setv out []) (asyncio.run ((fn :async [] @@ -49,7 +50,7 @@ (assert (= async-exits [1]))) (defn [async-test] test-quince-with-async [] - (setv (cut async-exits) []) + (.clear async-exits) (setv out []) (asyncio.run ((fn :async [] diff --git a/tests/resources/__init__.py b/tests/resources/__init__.py index 46142db78..d814425ef 100644 --- a/tests/resources/__init__.py +++ b/tests/resources/__init__.py @@ -17,23 +17,3 @@ def function_with_a_dash(): async_test = pytest.mark.skipif( not can_test_async, reason="`asyncio.run` not implemented" ) - - -async_exits = [] -class AsyncWithTest: - def __init__(self, val): - self.val = val - - async def __aenter__(self): - return self.val - - async def __aexit__(self, exc_type, exc, traceback): - async_exits.append(self.val) - - -async def async_loop(items): - import asyncio - - for x in items: - yield x - await asyncio.sleep(0) diff --git a/tests/resources/pydemo.hy b/tests/resources/pydemo.hy index 8c7ef7ae9..cda488456 100644 --- a/tests/resources/pydemo.hy +++ b/tests/resources/pydemo.hy @@ -1,7 +1,7 @@ ;; This Hy module is intended to concisely demonstrate all of ;; Python's major syntactic features for the purpose of testing hy2py. ;; It also tries out macros and reader macros to ensure they work with -;; hy2py. +;; hy2py. It's used as part of Hy's test suite as well as py2hy's. "This is a module docstring." (setv mystring (* "foo" 3)) @@ -102,25 +102,6 @@ Call me Ishmael. Some years ago—never mind how long precisely—having little (for [x ["fo" "fi" "fu"]] (setv for-block (+ x for-block))) -(try - (assert (= 1 0)) - (except [_ AssertionError] - (setv caught-assertion True)) - (finally - (setv ran-finally True))) - -(try - (raise (ValueError "payload")) - (except [e ValueError] - (setv myraise (str e)))) - -(try - 1 - (except [e ValueError] - (raise)) - (else - (setv ran-try-else True))) - (defn fun [a b [c 9] [from 10] #* args #** kwargs] "function docstring" [a b c from args (sorted (.items kwargs))]) @@ -147,6 +128,26 @@ Call me Ishmael. Some years ago—never mind how long precisely—having little (+= myglobal 1)) (set-global) +(setv finally-values []) +(defn mytry [error-type] + (try + (when error-type + (raise (error-type "payload"))) + (except [ZeroDivisionError] + "zero-div") + (except [e [ValueError TypeError]] + ["vt" (type e) e.args]) + (except [[]] + "never") + (except [e []] + "never2") + (except [] + "other") + (else + "else") + (finally + (.append finally-values 1)))) + (defclass C1 []) ; Force the creation of a `pass` statement. (defclass C2 [C1] @@ -174,8 +175,7 @@ Call me Ishmael. Some years ago—never mind how long precisely—having little (setv py-accum (py "''.join(map(str, pys_accum))")) (defn :async coro [] - (import asyncio - tests.resources [AsyncWithTest async-loop]) + (import asyncio) (await (asyncio.sleep 0)) (setv values ["a"]) (with [:async t (AsyncWithTest "b")] @@ -186,6 +186,19 @@ Call me Ishmael. Some years ago—never mind how long precisely—having little :async item (async-loop ["e" "f"]) item)) values) +(defclass AsyncWithTest [] + (defn __init__ [self val] + (setv self.val val)) + (defn :async __aenter__ [self] + self.val) + (defn :async __aexit__ [self exc-type exc traceback] + (.append async-exits self.val))) +(setv async-exits []) +(defn :async async-loop [items] + (import asyncio) + (for [x items] + (yield x) + (await (asyncio.sleep 0)))) (defmacro macaroni [expr] `[~expr ~expr]) diff --git a/tests/test_hy2py.py b/tests/test_hy2py.py index ec18515e4..f7c22a97f 100644 --- a/tests/test_hy2py.py +++ b/tests/test_hy2py.py @@ -1,3 +1,5 @@ +# This file is also used by py2hy. + import asyncio import itertools import math @@ -7,13 +9,12 @@ import hy.importer from hy import mangle from hy.compat import PYODIDE -from tests.resources import can_test_async def test_direct_import(): import tests.resources.pydemo - - assert_stuff(tests.resources.pydemo) + from tests.resources import can_test_async + assert_stuff(tests.resources.pydemo, can_test_async) @pytest.mark.skipif(PYODIDE, reason="subprocess.check_call not implemented on Pyodide") @@ -21,6 +22,7 @@ def test_hy2py_import(): import contextlib import os import subprocess + from tests.resources import can_test_async path = "tests/resources/pydemo_as_py.py" env = dict(os.environ) @@ -36,10 +38,10 @@ def test_hy2py_import(): finally: with contextlib.suppress(FileNotFoundError): os.remove(path) - assert_stuff(m) + assert_stuff(m, can_test_async) -def assert_stuff(m): +def assert_stuff(m, can_test_async): # This makes sure that automatically imported builtins go after docstrings. assert m.__doc__ == "This is a module docstring." @@ -121,10 +123,6 @@ def assert_stuff(m): assert m.while_block == "xxxxe" assert m.cont_and_break == "xyzxyzxxyzxy" assert m.for_block == "fufifo" - assert m.caught_assertion is True - assert m.ran_finally is True - assert m.myraise == "payload" - assert m.ran_try_else is True assert type(m.fun) is type(lambda x: x) assert m.fun.__doc__ == "function docstring" @@ -137,6 +135,13 @@ def assert_stuff(m): assert m.mydecorated.newattr == "hello" assert m.myglobal == 103 + assert m.mytry(ZeroDivisionError) == "zero-div" + assert m.mytry(ValueError) == ["vt", ValueError, ("payload",)] + assert m.mytry(TypeError) == ["vt", TypeError, ("payload",)] + assert m.mytry(OSError) == "other" + assert m.mytry(None) == "else" + assert len(m.finally_values) == 5 + class C: pass @@ -155,7 +160,9 @@ class C: assert m.py_accum == "01234" if can_test_async: + m.async_exits.clear() assert asyncio.run(m.coro()) == list("abcdef") + assert m.async_exits == ["b"] assert m.cheese == [1, 1] assert m.mac_results == ["x", "x"]