From e2719b42085d2e666adc0f13632e2b5b0d421c40 Mon Sep 17 00:00:00 2001 From: Tony Coder <407243179@qq.com> Date: Mon, 12 Oct 2026 19:14:39 +0800 Subject: [PATCH 1/2] fix: include ANTLR diagnostic in loads() ValidationError openqasm3 often raises QASM3ParsingError with an empty message while writing the real ANTLR diagnostic to stderr. Capture stderr during parse so ValidationError carries a useful reason for callers. Signed-off-by: Tony Coder <407243179@qq.com> --- CHANGELOG.md | 1 + src/pyqasm/entrypoint.py | 11 +++++++++-- tests/qasm3/test_entrypoint.py | 13 +++++++++++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a186e759..a5d06ff9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ Types of changes: ### Removed ### Fixed +- Fixed parse failures producing `ValidationError` with an empty message. ANTLR diagnostics written to stderr are now included in the exception text. ([#452](https://github.com/qBraid/pyqasm/pull/452), [#436](https://github.com/qBraid/pyqasm/issues/436)) ### Dependencies diff --git a/src/pyqasm/entrypoint.py b/src/pyqasm/entrypoint.py index 00aaf2db..fa5de99b 100644 --- a/src/pyqasm/entrypoint.py +++ b/src/pyqasm/entrypoint.py @@ -19,6 +19,8 @@ from __future__ import annotations +import contextlib +import io import os from typing import TYPE_CHECKING @@ -181,9 +183,14 @@ def loads(program: openqasm3.ast.Program | str, **kwargs) -> QasmModule: # after include resolution, so an opaque in a vendor include is rewritten too program, opaque_gates = rewrite_opaque_declarations(program) try: - program = openqasm3.parse(program) + # openqasm3 writes ANTLR diagnostics to stderr and often raises + # QASM3ParsingError with an empty message; capture stderr for callers. + stderr_buf = io.StringIO() + with contextlib.redirect_stderr(stderr_buf): + program = openqasm3.parse(program) except openqasm3.parser.QASM3ParsingError as err: - raise ValidationError(f"Failed to parse OpenQASM string: {err}") from err + detail = str(err).strip() or stderr_buf.getvalue().strip() + raise ValidationError(f"Failed to parse OpenQASM string: {detail}") from err elif not isinstance(program, openqasm3.ast.Program): raise TypeError("Input quantum program must be of type 'str' or 'openqasm3.ast.Program'.") elif include_dir is not None: diff --git a/tests/qasm3/test_entrypoint.py b/tests/qasm3/test_entrypoint.py index 4fa791e9..c0561ffc 100644 --- a/tests/qasm3/test_entrypoint.py +++ b/tests/qasm3/test_entrypoint.py @@ -70,6 +70,19 @@ def test_incorrect_qasm_version(): loads("OPENQASM 4.0;\n qubit q;") + +def test_loads_parse_error_includes_antlr_diagnostic(): + """Parse failures should surface the ANTLR diagnostic in ValidationError.""" + bad = "OPENQASM 3.0;\nqubit q;\nrx(pi / 2 q;\n" + with pytest.raises(ValidationError, match=r"no viable alternative|Failed to parse") as exc_info: + loads(bad) + msg = str(exc_info.value) + assert "Failed to parse OpenQASM string:" in msg + # must not be an empty reason after the colon + assert msg.rstrip() != "Failed to parse OpenQASM string:" + assert len(msg.split(":", 1)[-1].strip()) > 0 + + def test_incorrect_module_unroll_raises_error(): with pytest.raises(ValidationError): module = loads("OPENQASM 3.0;\n qubit q; h q[2]") From 05949dbbbb5d6c8ac040af128375d99a47ace706 Mon Sep 17 00:00:00 2001 From: Tony Coder <407243179@qq.com> Date: Tue, 13 Oct 2026 07:47:47 +0800 Subject: [PATCH 2/2] style: black-format test_entrypoint for Format CI Signed-off-by: Tony Coder <407243179@qq.com> --- tests/qasm3/test_entrypoint.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/qasm3/test_entrypoint.py b/tests/qasm3/test_entrypoint.py index c0561ffc..b6cab76e 100644 --- a/tests/qasm3/test_entrypoint.py +++ b/tests/qasm3/test_entrypoint.py @@ -25,7 +25,9 @@ from pyqasm.exceptions import ValidationError from tests.utils import check_unrolled_qasm -QASM_RESOURCES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "resources", "qasm") +QASM_RESOURCES_DIR = os.path.join( + os.path.dirname(os.path.abspath(__file__)), "resources", "qasm" +) def test_correct_file_read(): @@ -53,7 +55,8 @@ def test_incorrect_module_loading_file(): def test_incorrect_module_loading_program(): with pytest.raises( - TypeError, match="Input quantum program must be of type 'str' or 'openqasm3.ast.Program'." + TypeError, + match="Input quantum program must be of type 'str' or 'openqasm3.ast.Program'.", ): loads(1) @@ -70,11 +73,12 @@ def test_incorrect_qasm_version(): loads("OPENQASM 4.0;\n qubit q;") - def test_loads_parse_error_includes_antlr_diagnostic(): """Parse failures should surface the ANTLR diagnostic in ValidationError.""" bad = "OPENQASM 3.0;\nqubit q;\nrx(pi / 2 q;\n" - with pytest.raises(ValidationError, match=r"no viable alternative|Failed to parse") as exc_info: + with pytest.raises( + ValidationError, match=r"no viable alternative|Failed to parse" + ) as exc_info: loads(bad) msg = str(exc_info.value) assert "Failed to parse OpenQASM string:" in msg