Repository: biopython/biopython

Issue:
Add option to silence/capture stdout/stderr from Bio.PDB.PSEA
Spun from #3980.

Running `p-sea` through `Bio.PDB.PSEA` will always print output to standard out. We should refactor the code to use `subprocess.run` and add a keyword to the top-level `run_psea` function to control whether to print or capture the tool's output.

A mock implementation that adds some better error handling would be:

```import subprocess

def run_psea(pname, verbose=True):
    cmd = [...]
    try:
        p = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
    except subprocess.CalledProcessError as err:
        print(f"Command {cmd} failed")
        raise

    if verbose:
        print(p.stdout)
    ...
```


Discussion:
That sounds sensible, assuming we want to run p-sea on the user's behalf in the first place.

Cross reference #3344 deprecating our Bio.Application framework for wrapping command line tools.

Resolving patch:
diff --git a/Bio/PDB/PSEA.py b/Bio/PDB/PSEA.py
--- a/Bio/PDB/PSEA.py
+++ b/Bio/PDB/PSEA.py
@@ -17,11 +17,12 @@
 """
 
 import subprocess
+import os
 
 from Bio.PDB.Polypeptide import is_aa
 
 
-def run_psea(fname):
+def run_psea(fname, verbose=False):
     """Run PSEA and return output filename.
 
     Note that this assumes the P-SEA binary is called "psea" and that it is
@@ -30,12 +31,22 @@ def run_psea(fname):
     Note that P-SEA will write an output file in the current directory using
     the input filename with extension ".sea".
 
-    Note that P-SEA will write output to the terminal while run.
+    Note that P-SEA will not write output to the terminal while run unless
+     verbose is set to True.
     """
-    subprocess.call(["psea", fname])
     last = fname.split("/")[-1]
     base = last.split(".")[0]
-    return base + ".sea"
+    cmd = ["psea", fname]
+
+    p = subprocess.run(cmd, capture_output=True, universal_newlines=True)
+
+    if verbose:
+        print(p.stdout)
+
+    if not p.stderr.strip() and os.path.exists(base + ".sea"):
+        return base + ".sea"
+    else:
+        raise RuntimeError(f"Error running p-sea: {p.stderr}")
 
 
 def psea(pname):


Test patch:
diff --git a/Tests/test_PDB_PSEA.py b/Tests/test_PDB_PSEA.py
--- a/Tests/test_PDB_PSEA.py
+++ b/Tests/test_PDB_PSEA.py
@@ -5,9 +5,11 @@
 
 """Tests for PDB PSEA."""
 
+import io
 import os
 import unittest
 from subprocess import getoutput
+import sys
 
 from Bio import MissingExternalDependencyError
 from Bio.PDB import PDBParser
@@ -36,9 +38,21 @@ class TestPDBPSEA(unittest.TestCase):
     def tearDown(self):
         remove_sea_files()
 
-    def test_run_psea(self):
-        psae_run = run_psea("PDB/1A8O.pdb")
+    def test_run_psea_verbose(self):
+        captured_ouput = io.StringIO()
+        sys.stdout = captured_ouput
+        psae_run = run_psea("PDB/1A8O.pdb", verbose=True)
+        sys.stdout = sys.__stdout__
         self.assertEqual(psae_run, "1A8O.sea")
+        self.assertTrue(captured_ouput.getvalue())
+
+    def test_run_psea_quiet(self):
+        captured_ouput = io.StringIO()
+        sys.stdout = captured_ouput
+        psae_run = run_psea("PDB/1A8O.pdb", verbose=False)
+        sys.stdout = sys.__stdout__
+        self.assertEqual(psae_run, "1A8O.sea")
+        self.assertFalse(captured_ouput.getvalue())
 
     def test_psea(self):
         psae_run = psea("PDB/2BEG.pdb")