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 the interpretation of except [[]] #2636

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
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
3 changes: 3 additions & 0 deletions NEWS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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)
======================================================================
Expand Down
2 changes: 2 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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``.
Expand Down
30 changes: 17 additions & 13 deletions hy/core/result_macros.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions tests/native_tests/try.hy
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 4 additions & 3 deletions tests/native_tests/with.hy
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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 []
Expand All @@ -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 []
Expand Down
20 changes: 0 additions & 20 deletions tests/resources/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
57 changes: 35 additions & 22 deletions tests/resources/pydemo.hy
Original file line number Diff line number Diff line change
@@ -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))
Expand Down Expand Up @@ -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))])
Expand All @@ -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]
Expand Down Expand Up @@ -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")]
Expand All @@ -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])
Expand Down
25 changes: 16 additions & 9 deletions tests/test_hy2py.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# This file is also used by py2hy.

import asyncio
import itertools
import math
Expand All @@ -7,20 +9,20 @@
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")
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)
Expand All @@ -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."
Expand Down Expand Up @@ -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"
Expand All @@ -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

Expand All @@ -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"]
Expand Down