Repository: qutip/qutip

Issue:
Ambiguous error message for circuit plotting
**Describe the issue**
We use subprocess for the LaTeX plotting procedure. But the current error message for a failed subprocess is very ambiguous, which makes it very hard to debug.

For illustration purpose, I deliberately change the following line so that the input pdf file has a wrong name.
https://github.com/qutip/qutip/blob/403e466becbc5d36947ca15b9211d884df83f91a/qutip/qip/circuit_latex.py#L172
I changed it to e.g. `in_file = "pdf-that-does-not-exist.pdf"`. Running the code, the output error message is
```
CalledProcessError: Command '('magick', '-density', '100', 'aqcirc.pdf', 'qcirc.png')' returned non-zero exit status 1.
```
This is not very useful since the exit status 1 can have many reasons. It can also be that an additional dependency is missing (which is the reason I started to debug at the first place).
What I would like to see is the real system error message:
```
magick: unable to open image 'pdf-that-does-not-exist.pdf': No such file or directory
```

**Possible solution**
I didn't find a nice solution for this. Simply changing the parameter of `subprocess.run` to `stderr=subprocess.PIPE` doesn't work for me. I have to catch the exception and print the stderr message:
```
def _run_command(*args, **kwargs):
    try:
        functools.partial(subprocess.run, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE)(*args, **kwargs)
    except subprocess.CalledProcessError as err:
        raise RuntimeError(err.stderr)
```
which gives me the output
```
RuntimeError: b"magick: unable to open image 'aqcirc.pdf': No such file or directory @ error/blob.c/OpenBlob/3496.\r\n"
```
It's ok but still weird. I guess I overlooked some feature of `subprocess`.

@jakelishman I'm wondering if you have a better solution?


Discussion:
Looks sensible enough to me.  If you want to leak stderr through you can set `stderr=None`, but I think it's a better idea to capture it like you've done here so you have control.  You can also decode the bytes object at the end back into a unicode string with `err.stderr.decode(sys.stderr.encoding)`, which will get rid of the `b" "`.

There's no need for `functools.partial` in your solution any more; it could just be
```python
def _run_command(command, *args, **kwargs):
    try:
        return subprocess.run(
            command, *args,
            check=True, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE,
            **kwargs,
        )
    except subprocess.CalledProcessError as e:
        raise RuntimeError(e.stderr.decode(sys.stderr.encoding)) from None
```

Resolving patch:
diff --git a/qutip/qip/circuit_latex.py b/qutip/qip/circuit_latex.py
--- a/qutip/qip/circuit_latex.py
+++ b/qutip/qip/circuit_latex.py
@@ -33,6 +33,7 @@
 import collections
 import functools
 import os
+import sys
 import shutil
 import subprocess
 import tempfile
@@ -48,15 +49,20 @@
 """
 
 
-_run_command = functools.partial(subprocess.run, check=True,
-                                 stdout=subprocess.DEVNULL,
-                                 stderr=subprocess.DEVNULL)
-_run_command.__doc__ = \
+def _run_command(command, *args, **kwargs):
     """
-    Run a command with stdout and stderr explicitly thrown away, raising
-    `subprocess.CalledProcessError` if the command returned a non-zero exit
-    code.
+    Run a command with stdout explicitly thrown away, raising
+    `RuntimeError` with the system error message
+    if the command returned a non-zero exit code.
     """
+    try:
+        return subprocess.run(
+            command, *args,
+            check=True, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE,
+            **kwargs,
+        )
+    except subprocess.CalledProcessError as e:
+        raise RuntimeError(e.stderr.decode(sys.stderr.encoding)) from None
 
 
 def _force_remove(*filenames):


Test patch: