Repository: biopython/biopython

Issue:
Add .islower() and .isupper() methods to Seq?
Some time ago there was the aim to make the Seq object more 'string-like'. This could include .islower() and .isupper() methods. Relevant SO question: https://stackoverflow.com/questions/69176192/attributeerror-seq-object-has-no-attribute-islower-in-protparam-module-of-b 

Comments:

Patch:
diff --git a/Bio/Seq.py b/Bio/Seq.py
index bd8fd9eee..38f31bc33 100644
--- a/Bio/Seq.py
+++ b/Bio/Seq.py
@@ -281,6 +281,20 @@ class SequenceDataAbstractBaseClass(ABC):
         """Return a copy of data with all ASCII characters converted to lowercase."""
         return bytes(self).lower()
 
+    def isupper(self):
+        """Return True if all ASCII characters in data are uppercase.
+
+        If there are no cased characters, method returns False.
+        """
+        return bytes(self).isupper()
+
+    def islower(self):
+        """Return True if all ASCII characters in data are lowercase.
+
+        If there are no cased characters, method returns False.
+        """
+        return bytes(self).islower()
+
     def replace(self, old, new):
         """Return a copy with all occurrences of substring old replaced by new."""
         return bytes(self).replace(old, new)
@@ -1305,6 +1319,20 @@ class _SeqAbstractBaseClass(ABC):
         else:
             return self.__class__(data)
 
+    def isupper(self):
+        """Return True if all ASCII characters in data are uppercase.
+
+        If there are no cased characters, method returns False.
+        """
+        return self._data.isupper()
+
+    def islower(self):
+        """Return True if all ASCII characters in data are lowercase.
+
+        If there are no cased characters, method returns False.
+        """
+        return self._data.islower()
+
     def translate(
         self, table="Standard", stop_symbol="*", to_stop=False, cds=False, gap="-"
     ):
@@ -2891,6 +2919,22 @@ class _UndefinedSequenceData(SequenceDataAbstractBaseClass):
         # sequence of the same length
         return _UndefinedSequenceData(self._length)
 
+    def isupper(self):
+        """Return True if all ASCII characters in data are uppercase.
+
+        If there are no cased characters, method returns False.
+        """
+        # Character case is irrelevant for an undefined sequence
+        raise UndefinedSequenceError("Sequence content is undefined")
+
+    def islower(self):
+        """Return True if all ASCII characters in data are lowercase.
+
+        If there are no cased characters, method returns False.
+        """
+        # Character case is irrelevant for an undefined sequence
+        raise UndefinedSequenceError("Sequence content is undefined")
+
     def replace(self, old, new):
         """Return a copy with all occurrences of substring old replaced by new."""
         # Replacing substring old by new in an undefined sequence will result
@@ -3084,6 +3128,22 @@ class _PartiallyDefinedSequenceData(SequenceDataAbstractBaseClass):
         data = {start: seq.lower() for start, seq in self._data.items()}
         return _PartiallyDefinedSequenceData(self._length, data)
 
+    def isupper(self):
+        """Return True if all ASCII characters in data are uppercase.
+
+        If there are no cased characters, method returns False.
+        """
+        # Character case is irrelevant for an undefined sequence
+        raise UndefinedSequenceError("Sequence content is only partially defined")
+
+    def islower(self):
+        """Return True if all ASCII characters in data are lowercase.
+
+        If there are no cased characters, method returns False.
+        """
+        # Character case is irrelevant for an undefined sequence
+        raise UndefinedSequenceError("Sequence content is only partially defined")
+
     def translate(self, table, delete=b""):
         """Return a copy with each character mapped by the given translation table.
 
diff --git a/CONTRIB.rst b/CONTRIB.rst
index f554eb45b..d64d3f2b4 100644
--- a/CONTRIB.rst
+++ b/CONTRIB.rst
@@ -105,6 +105,7 @@ please open an issue on GitHub or mention it on the mailing list.
 - Eric Talevich <https://github.com/etal>
 - Erick Matsen <surname at fhcrc dot org>
 - Erik Cederstrand <https://github.com/ecederstrand>
+- Erik Whiting <https://github.com/erik-whiting>
 - Fabian Egli <https://github.com/fabianegli>
 - Fei Qi <https://github.com/qifei9>
 - Foen Peng <https://github.com/foenpeng>
diff --git a/NEWS.rst b/NEWS.rst
index 72026a49e..0a685501f 100644
--- a/NEWS.rst
+++ b/NEWS.rst
@@ -45,6 +45,7 @@ possible, especially the following contributors:
 - Tim Burke
 - Michiel de Hoon
 - Peter Cock
+- Erik  Whiting
 
 3 June 2021: Biopython 1.79
 ===========================


Test patch:
diff --git a/Tests/test_SeqIO_TwoBitIO.py b/Tests/test_SeqIO_TwoBitIO.py
index 262afea0c..47e3c992a 100644
--- a/Tests/test_SeqIO_TwoBitIO.py
+++ b/Tests/test_SeqIO_TwoBitIO.py
@@ -459,6 +459,14 @@ class TestBaseClassMethods(unittest.TestCase):
         self.assertEqual(seq2_twobit_lower, seq2_fasta_lower)
         self.assertEqual(seq2_twobit_lower[140:210], seq2_fasta_lower[140:210])
 
+    def test_isupper(self):
+        self.assertEqual(self.seq1_twobit.isupper(), self.seq1_fasta.isupper())
+        self.assertEqual(self.seq2_twobit.isupper(), self.seq2_fasta.isupper())
+
+    def test_islower(self):
+        self.assertEqual(self.seq1_twobit.islower(), self.seq1_fasta.islower())
+        self.assertEqual(self.seq2_twobit.islower(), self.seq2_fasta.islower())
+
     def test_replace(self):
         # seq.transcribe uses seq._data.replace
         self.assertEqual(self.seq1_twobit.transcribe(), self.seq1_fasta.transcribe())
diff --git a/Tests/test_Seq_objs.py b/Tests/test_Seq_objs.py
index 051d394a8..0e725a2e0 100644
--- a/Tests/test_Seq_objs.py
+++ b/Tests/test_Seq_objs.py
@@ -608,6 +608,28 @@ class StringMethodTests(unittest.TestCase):
             Seq("ABCD").lower(inplace=True)
         self.assertEqual(str(cm.exception), "Sequence is immutable")
 
+    def test_str_isupper(self):
+        """Check matches the python string isupper method."""
+        for example1 in self._examples:
+            str1 = str(example1)
+            if isinstance(example1, _UndefinedSequenceData):
+                with self.assertRaises(UndefinedSequenceError):
+                    example1.isupper()
+            else:
+                example1 = example1.isupper()
+            self.assertEqual(example1, str1.isupper())
+
+    def test_str_islower(self):
+        """Check matches the python string islower method."""
+        for example1 in self._examples:
+            str1 = str(example1)
+            if isinstance(example1, _UndefinedSequenceData):
+                with self.assertRaises(UndefinedSequenceError):
+                    example1.islower()
+            else:
+                example1 = example1.islower()
+            self.assertEqual(example1, str1.islower())
+
     def test_str_replace(self):
         """Check matches the python string replace method."""
         s = Seq("AAGTACGT")
diff --git a/Tests/test_seq.py b/Tests/test_seq.py
index e861d4494..e3237233f 100644
--- a/Tests/test_seq.py
+++ b/Tests/test_seq.py
@@ -175,6 +175,8 @@ class TestSeqStringMethods(unittest.TestCase):
         for a in self.dna + self.rna + self.nuc + self.protein:
             self.assertEqual(a.lower(), str(a).lower())
             self.assertEqual(a.upper(), str(a).upper())
+            self.assertEqual(a.islower(), str(a).islower())
+            self.assertEqual(a.isupper(), str(a).isupper())
             self.assertEqual(a.strip(), str(a).strip())
             self.assertEqual(a.lstrip(), str(a).lstrip())
             self.assertEqual(a.rstrip(), str(a).rstrip())
@@ -184,22 +186,34 @@ class TestSeqStringMethods(unittest.TestCase):
         lseq = seq.lower()
         self.assertEqual(lseq, "acgt")
         self.assertEqual(seq, "ACgt")
+        self.assertTrue(lseq.islower())
+        self.assertFalse(seq.islower())
         lseq = seq.lower(inplace=False)
         self.assertEqual(lseq, "acgt")
         self.assertEqual(seq, "ACgt")
+        self.assertTrue(lseq.islower())
+        self.assertFalse(seq.islower())
         lseq = seq.lower(inplace=True)
         self.assertEqual(lseq, "acgt")
         self.assertIs(lseq, seq)
+        self.assertTrue(lseq.islower())
+        self.assertTrue(lseq.islower())
         seq = Seq.MutableSeq("ACgt")
         useq = seq.upper()
         self.assertEqual(useq, "ACGT")
         self.assertEqual(seq, "ACgt")
+        self.assertTrue(useq.isupper())
+        self.assertFalse(seq.isupper())
         useq = seq.upper(inplace=False)
         self.assertEqual(useq, "ACGT")
         self.assertEqual(seq, "ACgt")
+        self.assertTrue(useq.isupper())
+        self.assertFalse(seq.isupper())
         useq = seq.upper(inplace=True)
         self.assertEqual(useq, "ACGT")
         self.assertIs(useq, seq)
+        self.assertTrue(useq.isupper())
+        self.assertTrue(seq.isupper())
 
     def test_hash(self):
         with warnings.catch_warnings(record=True):