diff --git a/CHANGELOG.md b/CHANGELOG.md index e1ce1b8..728c3c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ Types of changes: ### Fixed - Fixed `loads("")` / whitespace-only input raising `AttributeError` instead of `ValidationError`. Empty and whitespace programs now raise `ValidationError` like other parse failures. ([#451](https://github.com/qBraid/pyqasm/pull/451), [#437](https://github.com/qBraid/pyqasm/issues/437)) +- 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 7e55ea3..5a4359f 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,10 +183,15 @@ 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, AttributeError) as err: # openqasm3 can raise AttributeError on empty/whitespace-only input - 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 11d5253..dccca69 100644 --- a/tests/qasm3/test_entrypoint.py +++ b/tests/qasm3/test_entrypoint.py @@ -53,7 +53,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,6 +71,18 @@ 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]")