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 


Discussion:
I am not sure when it would be useful. The `islower` and `isupper` methods are used in a few places in Biopython, but there it would probably be faster to just call `upper` or `lower` always.
Btw the three instances of `islower` in `Bio.SeqUtils` contain a bug: Sequences with mixed case are not converted to upper case. Protein sequences in all upper case or all lower case are handled correctly, but sequences with mixed case are not:

```python
>>> from Bio.SeqUtils.ProtParam import ProteinAnalysis
>>> p = ProteinAnalysis("MVKLS")
>>> p.secondary_structure_fraction()
(0.4, 0.2, 0.4)
>>> p = ProteinAnalysis("mvkls")
>>> p.secondary_structure_fraction()
(0.4, 0.2, 0.4)
>>> p = ProteinAnalysis("MVkls")
>>> p.secondary_structure_fraction()
(0.2, 0.0, 0.2)
```

The bug occurs in the initialization of `ProteinAnalysis`:
```python
        if prot_sequence.islower():
            self.sequence = Seq(prot_sequence.upper())
        else:
            self.sequence = Seq(prot_sequence)
```
This should be
```python
        if not prot_sequence.isupper():
            self.sequence = Seq(prot_sequence.upper())
        else:
            self.sequence = Seq(prot_sequence)
```
However, `isupper` is much slower than `upper` (about 12 times slower), so you may as well call `upper` without checking for `isupper`.

That looks like an easy fix - Chris do you want to do that?

As to the original issue, I favour adding ``.isupper()`` and ``islower()`` following string-like behaviour expectations (and indeed, the bytes object does this too), even if usage will be rare.
@peterjc Hi, I'd like to work on this issue. If it isn't assigned already, can you please assign this to me? Also, since I'm new to the project, can you please point me to the relevant files, that I'd need to look at?
@peterjc As to the original issue, I favour adding `.isupper()` and `islower()` following string-like behaviour expectations (and indeed, the bytes object does this too), even if usage will be rare.

I still think we should think of a use case first.
Use case: Many external sequence analysis tools use upper/lower case to indicate masking part of a sequence, thus ``my_seq.isupper()`` would indicate no lower-case masking is present.
@peterjc OK, that is a good point. Then I am also in favor of adding `islower` and `isupper`.
@deepto98 The changes would be in file https://github.com/biopython/biopython/blob/master/Bio/Seq.py

Make sure you work from the latest code - this file has changed a lot recently. I think you would be adding the methods to the ``SequenceDataAbstractBaseClass`` (which was only added in Biopython 1.79)
@deepto98 Are you still motivated to work on this?

The function definitions in https://github.com/biopython/biopython/blob/master/Bio/Seq.py#L283  should be quite simple, e.g.

```
    def isupper(self):
        """Return True if all cased characters in data are uppercase
        and there is at least one cased character, False otherwise.
        """
        return bytes(self).isupper()
```

Similar for `.islower()`

Then perhaps a couple of simple unit tests here: https://github.com/biopython/biopython/blob/master/Tests/test_seq.py#L930 
Thanks Chris. Note that docstring would need a little tweaking to pass our style checks, basing it off the Python method's docstring make sense.
@chris-rands  It will be a bit more complicated than that, because the sequence may be undefined or partially defined. In such cases, `bytes(self)` will raise a `Bio.Seq.UndefinedSequenceError`. This may work better:
```
    def isupper(self):
        """Return True if all cased characters in data are uppercase
        and there is at least one cased character, False otherwise.
        """
        return self._data.isupper()
```
and then we need to define `isupper` in the base class `SequenceDataAbstractBaseClass`, and override that method when needed (e.g. in `_UndefinedSequenceData` and `_PartiallyDefinedSequenceData`).

We have to decide what `isupper` should return for undefined and partially defined sequences.
Returning ``UndefinedSequenceError`` seems reasonable there.
@chris-rands, @mdehoon and @peterjc if this still needs assigning, and the other user (@deepto98 ) hasn't claimed it, do you mind if I take a stab at it? It would be my first contribution to Biopython so I will likely have a slow velocity and maybe some learning curves to climb but if you're cool with that I'd love to take it.
That sounds like a good plan, please give it a go.
Should I add these methods to the deprecated `UnknownSeq` class as well? If so, should it also raise an `UndefinedSequenceError`? Or perhaps they should just behave normally? Thoughts?
@erik-whiting  I don't think it is needed to add these methods to `UnknownSeq`.

Resolving patch:
diff --git a/Bio/Seq.py b/Bio/Seq.py
--- a/Bio/Seq.py
+++ b/Bio/Seq.py
@@ -281,6 +281,20 @@ def lower(self):
         """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)
@@ -1301,6 +1315,20 @@ def lower(self, inplace=False):
         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="-"
     ):
@@ -2887,6 +2915,22 @@ def lower(self):
         # 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
@@ -3080,6 +3124,22 @@ def lower(self):
         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.
 


Test patch:
diff --git a/Tests/test_SeqIO_TwoBitIO.py b/Tests/test_SeqIO_TwoBitIO.py
--- a/Tests/test_SeqIO_TwoBitIO.py
+++ b/Tests/test_SeqIO_TwoBitIO.py
@@ -459,6 +459,14 @@ def test_lower(self):
         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
--- a/Tests/test_Seq_objs.py
+++ b/Tests/test_Seq_objs.py
@@ -608,6 +608,28 @@ def test_str_lower(self):
             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
--- a/Tests/test_seq.py
+++ b/Tests/test_seq.py
@@ -165,6 +165,8 @@ def test_string_methods(self):
         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())
@@ -174,22 +176,34 @@ def test_mutableseq_upper_lower(self):
         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):