Repository: dakk/qlasskit (language: python)

Issue:
py2qasm cli tool
Create a cli tool in the [`qlasskit/tools`](https://github.com/dakk/qlasskit/tree/master/qlasskit/tools) package called `py2qasm` that receives a python script (file or stdin) in input and outputs qasm code. The python script should contain at least one qlassf function. 

- If not specified by --input-file/-i, read from stdin (-).
- If not specified by --entrypoint/-e, func_name is the last qlassf defined.
- If not specified by --output/-o, output to stdout
- If not specified by --compiler/-c use internal compiler (or tweedledum)
- If not specified by --qasm-version/-q use v3 qasm version

```py2qasm -i pyfile [-e func_name] [-c internal] -o qasm_file```

Provide also:
- `-h\--help` for usage
- `-v\--version` for qlasskit version

Requirements:
- Code should be placed on [`qlasskit/tools`](https://github.com/dakk/qlasskit/tree/master/qlasskit/tools)
- Use [`argparse`](https://docs.python.org/3/library/argparse.html) for argument parsing
- Use [`qlasskit.tools.utils`](https://github.com/dakk/qlasskit/blob/master/qlasskit/tree/master/qlasskit/tools/utils.py) for getting qlassfs from file / string 
- A test case must be provided in [`test_tools.py`](https://github.com/dakk/qlasskit/blob/master/test/test_tools.py)
- Keep an eye on issue #28 

Please read [**CONTRIBUTING.md**](https://github.com/dakk/qlasskit/blob/master/CONTRIBUTING.md) before contributing.



Discussion / hints:
Hi, @dakk !
I'd like to work on this issue as a part of unitaryhack.
Hi, wonderful, please go on and let me know if you need anything.
Hi, @dakk! Can you please share a couple of sample python files to be transpiled?
There is one in the test_tools file linked above. If you need more you can find some example code in test cases and in the docs; let me know if you need something different 

Fix patch:
diff --git a/qlasskit/tools/py2qasm.py b/qlasskit/tools/py2qasm.py
new file mode 100644
index 00000000..8e6aa472
--- /dev/null
+++ b/qlasskit/tools/py2qasm.py
@@ -0,0 +1,100 @@
+#!/usr/bin/env python3
+
+# Copyright 2023-2024 Davide Gessa
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import argparse
+import sys
+
+import qlasskit
+from qlasskit.qcircuit import exporter_qasm
+from qlasskit.qlassfun import QlassF
+from qlasskit.tools.utils import parse_str
+
+from .tools import find_last_qlassf
+
+
+def read_input(input_file):
+    if input_file == "-":
+        return sys.stdin.read()
+    with open(input_file, "r") as file:
+        return file.read()
+
+
+def convert_to_quasm(qlassf: QlassF, compiler="internal", version=3):
+    qlassf.compile(compiler=compiler)
+    qcirc = qlassf.circuit()
+    exporter = exporter_qasm.QasmExporter(version=version)
+    return exporter.export(qcirc, mode="circuit")
+
+
+def output_result(result, output_file):
+    if output_file == "-":
+        print(result)
+    else:
+        with open(output_file, "w") as file:
+            file.write(str(result))
+
+
+def main():
+    parser = argparse.ArgumentParser(
+        description="Convert qlassf functions in a Python script to qasm code expressions."
+    )
+    parser.add_argument(
+        "-i", "--input-file", default="-", help="Input file (default: stdin)"
+    )
+    parser.add_argument("-e", "--entrypoint", help="Entrypoint function name")
+    parser.add_argument(
+        "-o", "--output", default="-", help="Output file (default: stdout)"
+    )
+    parser.add_argument(
+        "-c",
+        "--compiler",
+        choices=["internal", "tweedledum", "recompiler"],
+        default="internal",
+        help="QASM compiler (default: internal)",
+    )
+    parser.add_argument(
+        "-q",
+        "--qasm-version",
+        choices=["2.0", "3.0"],
+        default="3.0",
+        help="QASM version (default: 3.0)",
+    )
+    parser.add_argument(
+        "-v", "--version", action="version", version=f"qlasskit {qlasskit.__version__}"
+    )
+
+    args = parser.parse_args()
+
+    script = read_input(args.input_file)
+    qlassf_list = parse_str(script)
+
+    if args.entrypoint:
+        qlassf = next((f[1] for f in qlassf_list if f[0] == args.entrypoint), None)
+    else:
+        qlassf = find_last_qlassf(qlassf_list)
+
+    compiler = args.compiler
+    version = 3 if args.qasm_version == "3.0" else 2
+
+    if qlassf:
+        bool_expr = convert_to_quasm(qlassf, compiler=compiler, version=version)
+        output_result(bool_expr, args.output)
+    else:
+        print("No qlassf function found", file=sys.stderr)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/setup.py b/setup.py
index ab5712ec..5412891f 100644
--- a/setup.py
+++ b/setup.py
@@ -64,6 +64,7 @@
     entry_points={
         "console_scripts": [
             "py2bexp=qlasskit.tools.py2bexp:main",
+            "py2qasm=qlasskit.tools.py2qasm:main",
         ]
     },
 )


Test patch:
diff --git a/test/test_tools.py b/test/test_tools.py
index d3e7e1c0..a59c75c5 100644
--- a/test/test_tools.py
+++ b/test/test_tools.py
@@ -20,11 +20,13 @@
 
 import sympy
 from sympy.logic.boolalg import And, Not, Or, is_cnf, is_dnf, is_nnf
-
 # from sympy.logic.boolalg import to_anf
 from sympy.logic.utilities.dimacs import load
 
 import qlasskit
+from qlasskit.qcircuit import exporter_qasm
+from qlasskit.qlassfun import qlassf
+
 from qlasskit.tools import utils
 
 dummy_script = """
@@ -230,3 +232,149 @@ def test_stdin_input(self):
         expr = sympy.parse_expr(result.stdout)
         expected = sympy.parse_expr("(x | y | ~z) & (z | ~y)")
         self.assertTrue(expr.equals(expected))
+
+
+dummy_qlassf = """
+def c(x: bool, y: bool, z: bool) -> bool:
+    return (x or y or not z) and (not y or z)
+"""
+
+
+class TestPy2Qasm(unittest.TestCase):
+
+    def setUp(self):
+        # Create a temporary file to hold the dummy script
+        self.temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".py")
+        self.temp_file.write(dummy_script.encode())
+        self.temp_file.close()
+
+    def tearDown(self):
+        # Remove the temporary file
+        os.unlink(self.temp_file.name)
+
+    def run_command(self, args, stdin_input=None):
+        try:
+            result = subprocess.run(
+                args, input=stdin_input, capture_output=True, text=True, check=True
+            )
+            return result
+        except subprocess.CalledProcessError as e:
+            print(
+                f"Command '{' '.join(e.cmd)}' returned non-zero exit status {e.returncode}"
+            )
+            print(f"Standard output:\n{e.stdout}")
+            print(f"Standard error:\n{e.stderr}")
+            raise
+
+    # def test_help(self):
+    #     result = self.run_command(["python", "-m", "qlasskit.tools.py2qasm", "--help"])
+
+    def test_version(self):
+        result = self.run_command(
+            ["python", "-m", "qlasskit.tools.py2qasm", "--version"]
+        )
+        self.assertTrue(result.stdout == f"qlasskit {qlasskit.__version__}\n")
+
+    def test_output_to_stdout(self):
+        result = self.run_command(
+            ["python", "-m", "qlasskit.tools.py2qasm", "-i", self.temp_file.name]
+        )
+        print(result.stdout)
+        qf = qlassf(dummy_qlassf, to_compile=True, compiler="internal")
+        exporter = exporter_qasm.QasmExporter(version=3)
+        expected = exporter.export(qf.circuit(), mode="circuit") + "\n"
+        print(expected)
+        self.assertTrue(result.stdout == expected)
+
+    def test_specific_entrypoint(self):
+        result = self.run_command(
+            [
+                "python",
+                "-m",
+                "qlasskit.tools.py2qasm",
+                "-i",
+                self.temp_file.name,
+                "-e",
+                "a",
+            ]
+        )
+        print(result.stdout)
+        qf = qlassf(
+            "def a(b: bool) -> bool:\n\treturn not b",
+            to_compile=True,
+            compiler="internal",
+        )
+        exporter = exporter_qasm.QasmExporter(version=3)
+        expected = exporter.export(qf.circuit(), mode="circuit") + "\n"
+        self.assertTrue(result.stdout == expected)
+
+    def test_output_to_file(self):
+        with tempfile.NamedTemporaryFile(delete=False) as temp_output:
+            output_file = temp_output.name
+        try:
+            self.run_command(
+                [
+                    "python",
+                    "-m",
+                    "qlasskit.tools.py2qasm",
+                    "-i",
+                    self.temp_file.name,
+                    "-o",
+                    output_file,
+                ]
+            )
+            with open(output_file, "r") as f:
+                content = f.read()
+                print(content)
+                qf = qlassf(dummy_qlassf, to_compile=True, compiler="internal")
+                exporter = exporter_qasm.QasmExporter(version=3)
+                expected = exporter.export(qf.circuit(), mode="circuit")
+                self.assertTrue(content == expected)
+        finally:
+            os.unlink(output_file)
+
+    def test_stdin_input(self):
+        result = self.run_command(
+            ["python", "-m", "qlasskit.tools.py2qasm"], stdin_input=dummy_script
+        )
+        print(result.stdout)
+        qf = qlassf(dummy_qlassf, to_compile=True, compiler="internal")
+        exporter = exporter_qasm.QasmExporter(version=3)
+        expected = exporter.export(qf.circuit(), mode="circuit") + "\n"
+        self.assertTrue(result.stdout == expected)
+
+    def test_qasm_version_2(self):
+        result = self.run_command(
+            [
+                "python",
+                "-m",
+                "qlasskit.tools.py2qasm",
+                "-i",
+                self.temp_file.name,
+                "-q",
+                "2.0",
+            ]
+        )
+        print(result.stdout)
+        qf = qlassf(dummy_qlassf, to_compile=True, compiler="internal")
+        exporter = exporter_qasm.QasmExporter(version=2)
+        expected = exporter.export(qf.circuit(), mode="circuit") + "\n"
+        self.assertTrue(result.stdout == expected)
+
+    def test_qasm_version_3(self):
+        result = self.run_command(
+            [
+                "python",
+                "-m",
+                "qlasskit.tools.py2qasm",
+                "-i",
+                self.temp_file.name,
+                "-q",
+                "3.0",
+            ]
+        )
+        print(result.stdout)
+        qf = qlassf(dummy_qlassf, to_compile=True, compiler="internal")
+        exporter = exporter_qasm.QasmExporter(version=3)
+        expected = exporter.export(qf.circuit(), mode="circuit") + "\n"
+        self.assertTrue(result.stdout == expected)