Repository: biopython/biopython

Issue:
Add .removeprefix and .removesuffix to Seq object (and subclasses)
Python 3.9 adds these new string methods, which to me would apply equally well to sequence manipulation too - for example removing a start or stop codon, or prefixed barcode/adaptor sequences. There are likely other good motivating examples.

https://docs.python.org/3.9/whatsnew/3.9.html
https://www.python.org/dev/peps/pep-0616/



Discussion:
Do you mind if I give it a try?
Sure. Have a look at https://github.com/biopython/biopython/blob/master/CONTRIBUTING.rst and the testing chapter in the tutorial http://biopython.org/DIST/docs/tutorial/Tutorial.html

Resolving patch:
diff --git a/Bio/Seq.py b/Bio/Seq.py
--- a/Bio/Seq.py
+++ b/Bio/Seq.py
@@ -274,6 +274,32 @@ def rstrip(self, chars=None):
         """
         return bytes(self).rstrip(chars)
 
+    def removeprefix(self, prefix):
+        """Remove the prefix if present."""
+        # Want to do just this, but need Python 3.9+
+        # return bytes(self).removeprefix(prefix)
+        data = bytes(self)
+        try:
+            return data.removeprefix(prefix)
+        except AttributeError:
+            if data.startswith(prefix):
+                return data[len(prefix) :]
+            else:
+                return data
+
+    def removesuffix(self, suffix):
+        """Remove the suffix if present."""
+        # Want to do just this, but need Python 3.9+
+        # return bytes(self).removesuffix(suffix)
+        data = bytes(self)
+        try:
+            return data.removesuffix(suffix)
+        except AttributeError:
+            if data.startswith(suffix):
+                return data[: -len(suffix)]
+            else:
+                return data
+
     def upper(self):
         """Return a copy of data with all ASCII characters converted to uppercase."""
         return bytes(self).upper()
@@ -1230,6 +1256,91 @@ def rstrip(self, chars=None, inplace=False):
         else:
             return self.__class__(data)
 
+    def removeprefix(self, prefix, inplace=False):
+        """Return a new Seq object with prefix (left) removed.
+
+        This behaves like the python string method of the same name.
+
+        e.g. Removing a start Codon:
+
+        >>> from Bio.Seq import Seq
+        >>> my_seq = Seq("ATGGTGTGTGT")
+        >>> my_seq
+        Seq('ATGGTGTGTGT')
+        >>> my_seq.removeprefix('ATG')
+        Seq('GTGTGTGT')
+
+        As ``Seq`` objects are immutable, a ``TypeError`` is raised if
+        ``removeprefix`` is called on a ``Seq`` object with ``inplace=True``.
+
+        See also the removesuffix method.
+        """
+        if isinstance(prefix, _SeqAbstractBaseClass):
+            prefix = bytes(prefix)
+        elif isinstance(prefix, str):
+            prefix = prefix.encode("ASCII")
+        try:
+            data = self._data.removeprefix(prefix)
+        except TypeError:
+            raise TypeError(
+                "argument must be a string, Seq, MutableSeq, or bytes-like object"
+            ) from None
+        except AttributeError:
+            # Fall back for pre-Python 3.9
+            data = self._data
+            if data.startswith(prefix):
+                data = data[len(prefix) :]
+        if inplace:
+            if not isinstance(self._data, bytearray):
+                raise TypeError("Sequence is immutable")
+            self._data[:] = data
+            return self
+        else:
+            return self.__class__(data)
+
+    def removesuffix(self, suffix, inplace=False):
+        """Return a new Seq object with suffix (right) removed.
+
+        This behaves like the python string method of the same name.
+
+        e.g. Removing a stop codon:
+
+        >>> from Bio.Seq import Seq
+        >>> my_seq = Seq("GTGTGTGTTAG")
+        >>> my_seq
+        Seq('GTGTGTGTTAG')
+        >>> stop_codon = Seq("TAG")
+        >>> my_seq.removesuffix(stop_codon)
+        Seq('GTGTGTGT')
+
+        As ``Seq`` objects are immutable, a ``TypeError`` is raised if
+        ``removesuffix`` is called on a ``Seq`` object with ``inplace=True``.
+
+        See also the removeprefix method.
+        """
+        if isinstance(suffix, _SeqAbstractBaseClass):
+            suffix = bytes(suffix)
+        elif isinstance(suffix, str):
+            suffix = suffix.encode("ASCII")
+        try:
+            data = self._data.removesuffix(suffix)
+        except TypeError:
+            raise TypeError(
+                "argument must be a string, Seq, MutableSeq, or bytes-like object"
+            ) from None
+        except AttributeError:
+            # Fall back for pre-Python 3.9
+            data = self._data
+            if data.endswith(suffix):
+                data = data[: -len(suffix)]
+        if inplace:
+            if not isinstance(self._data, bytearray):
+                raise TypeError("Sequence is immutable")
+            self._data[:] = data
+            return self
+        else:
+            return self.__class__(data)
+
     def upper(self, inplace=False):
         """Return the sequence in upper case.
 


Test patch:
diff --git a/Tests/test_Seq_objs.py b/Tests/test_Seq_objs.py
--- a/Tests/test_Seq_objs.py
+++ b/Tests/test_Seq_objs.py
@@ -399,6 +399,30 @@ def test_str_lstrip(self):
             s.lstrip("ACGT", inplace=True)
         self.assertEqual(str(cm.exception), "Sequence is immutable")
 
+    def test_str_removeprefix(self):
+        """Check matches the python string removeprefix method."""
+        self.assertRaises(TypeError, Seq("ACGT").removeprefix, 7)
+        self.assertRaises(TypeError, Seq("ACGT").removeprefix, 7.0)
+        self.assertRaises(TypeError, Seq("ACGT").removeprefix, None)
+        self.assertRaises(
+            UndefinedSequenceError, Seq(data=None, length=1000).removeprefix, 7.0
+        )
+        self.assertRaises(
+            UndefinedSequenceError, Seq(data=None, length=1000).removeprefix, "A"
+        )
+
+    def test_str_removesuffix(self):
+        """Check matches the python string removesuffix method."""
+        self.assertRaises(TypeError, Seq("ACGT").removesuffix, 7)
+        self.assertRaises(TypeError, Seq("ACGT").removesuffix, 7.0)
+        self.assertRaises(TypeError, Seq("ACGT").removesuffix, None)
+        self.assertRaises(
+            UndefinedSequenceError, Seq(data=None, length=1000).removesuffix, 7.0
+        )
+        self.assertRaises(
+            UndefinedSequenceError, Seq(data=None, length=1000).removesuffix, "A"
+        )
+
     def test_str_rstrip(self):
         """Check matches the python string rstrip method."""
         self._test_method("rstrip")
diff --git a/Tests/test_seq.py b/Tests/test_seq.py
--- a/Tests/test_seq.py
+++ b/Tests/test_seq.py
@@ -295,6 +295,21 @@ def test_stripping_characters(self):
                 self.assertEqual(a.strip(char), str(a).strip(str_char))
                 self.assertEqual(a.lstrip(char), str(a).lstrip(str_char))
                 self.assertEqual(a.rstrip(char), str(a).rstrip(str_char))
+                try:
+                    removeprefix = str(a).removeprefix(str_char)
+                    removesuffix = str(a).removesuffix(str_char)
+                except AttributeError:
+                    if str(a).startswith(str_char):
+                        removeprefix = str(a)[len(str_char) :]
+                    else:
+                        removeprefix = str(a)
+                    if str_char and str(a).endswith(str_char):
+                        removesuffix = str(a)[: -len(str_char)]
+                    else:
+                        removesuffix = str(a)
+
+                self.assertEqual(a.removeprefix(char), removeprefix)
+                self.assertEqual(a.removesuffix(char), removesuffix)
 
     def test_finding_characters(self):
         for a in self.dna + self.rna + self.nuc + self.protein: