Repository: biopython/biopython

Issue:
pairwise2 can no longer be parallelized accross multiple processes
### Setup

I am reporting a problem with Biopython version, Python version, and operating
system as follows:
```
>>> import sys; print(sys.version)
3.8.10 (default, Mar 15 2022, 12:22:08) 
[GCC 9.4.0]
>>> import platform; print(platform.python_implementation()); print(platform.platform())
CPython
Linux-5.4.0-107-generic-x86_64-with-glibc2.29
>>> import Bio; print(Bio.__version__)
1.79
>>> 
```

(*Please copy and run the above in your Python, and copy-and-paste the output*)

### Expected behaviour

Previously (in version 1.77 for instance) it was possible to parallelize multiple alignment operations through multiprocessing.Pool.

This is important, since those are CPU intensive operations and can significantly speed up workloads.

### Actual behaviour

The job throws an exception of `Reason: 'PicklingError("Can't pickle <class 'Bio.pairwise2.Alignment'>: attribute lookup Alignment on Bio.pairwise2 failed")'`

### Steps to reproduce
```
def align_seqs(seq_g):
    from Bio import pairwise2
    return pairwise2.align.localds(seq_g[0], seq_g[1], match_dic, -8, -0.01)[0]

p=Pool(8)
p.map(align_seqs, genes, chunksize=16384)
```

In previous versions this returned a list of tuples, which are serializable. Since pull request 2603 [https://github.com/biopython/biopython/pull/2603] it returns a local named tuple which is unpicklable.

I could submit a pull request to fix this, but the pairwise2 module being unmaintained I am not sure it'd be accepted.



Discussion:
Yes, this does sound like an unintended side effect of #2603.

What do you think @MarkusPiotrowski, is this still worth trying to fix for Biopython 1.80 (where we are also deprecating the module)?
@EFraim  Can you use the new `PairwiseAligner` in `Bio.Align` instead?
I guess it is still worth trying to fix it. @EFraim: If you see a chance to fix this, I'd welcome a PR.

Resolving patch:
diff --git a/Bio/pairwise2.py b/Bio/pairwise2.py
--- a/Bio/pairwise2.py
+++ b/Bio/pairwise2.py
@@ -286,6 +286,8 @@
 
 MAX_ALIGNMENTS = 1000  # maximum alignments recovered in traceback
 
+Alignment = namedtuple("Alignment", ("seqA, seqB, score, start, end"))
+
 
 class align:
     """Provide functions that do alignments.
@@ -1152,7 +1154,6 @@ def _clean_alignments(alignments):
     Remove duplicates, make sure begin and end are set correctly, remove
     empty alignments.
     """
-    Alignment = namedtuple("Alignment", ("seqA, seqB, score, start, end"))
     unique_alignments = []
     for align in alignments:
         if align not in unique_alignments:


Test patch:
diff --git a/Tests/pairwise2_testCases.py b/Tests/pairwise2_testCases.py
--- a/Tests/pairwise2_testCases.py
+++ b/Tests/pairwise2_testCases.py
@@ -11,7 +11,7 @@
 with or without complementing C extensions.
 
 """
-
+import pickle
 import unittest
 import warnings
 
@@ -859,6 +859,12 @@ def test_clean_alignments(self):
         result = pairwise2._clean_alignments(alns)
         self.assertEqual(expected, result)
 
+    def test_alignments_can_be_pickled(self):
+        alns = [("ACCGT", "AC-G-", 3.0, 0, 4)]
+        expected = [("ACCGT", "AC-G-", 3.0, 0, 4)]
+        result = pickle.loads(pickle.dumps(pairwise2._clean_alignments(alns)))
+        self.assertEqual(expected, result)
+
     def test_print_matrix(self):
         """``print_matrix`` prints nested lists as nice matrices."""
         import sys