Repository: cogent3/cogent3
Pull request #2070 (2024-10-28 02:10:19)

ENH: added AlphabetABC.moltype property
[NEW] CodonAlphabet now inherits from AlphabetABC

[NEW] moltype property based on new_alphabet._alphabet_moltype_map dict
    which is populated when alphabets are built by a moltype. Returns
    None otherwise.

## Summary by Sourcery

Add a moltype property to the AlphabetABC class and update CodonAlphabet to inherit from AlphabetABC. Implement tests to ensure the correct functionality of the moltype property across various alphabet instances.

New Features:
- Introduce a moltype property to the AlphabetABC class, allowing retrieval of the molecular type associated with an alphabet.

Enhancements:
- CodonAlphabet now inherits from AlphabetABC, aligning it with the abstract base class structure.

Tests:
- Add tests to verify the moltype property for different alphabets, including cases where the moltype is defined and where it is not.


Patch:
diff --git a/src/cogent3/core/new_alphabet.py b/src/cogent3/core/new_alphabet.py
index a2984e56f..d2a66bf10 100644
--- a/src/cogent3/core/new_alphabet.py
+++ b/src/cogent3/core/new_alphabet.py
@@ -141,6 +141,10 @@ class AlphabetABC(ABC):
     @abstractmethod
     def from_rich_dict(cls, data: dict) -> None: ...
 
+    @property
+    def moltype(self) -> typing.Union["MolType", None]:
+        return _alphabet_moltype_map.get(self, None)
+
 
 class MonomerAlphabetABC(ABC):
     @abstractmethod
@@ -382,7 +386,10 @@ class CharAlphabet(tuple, AlphabetABC, MonomerAlphabetABC):
         words += (gap,) if include_gap and gap else ()
         words += (missing,) if include_gap and missing else ()
 
-        return KmerAlphabet(words=words, monomers=self, gap=gap, k=k, missing=missing)
+        kalpha = KmerAlphabet(words=words, monomers=self, gap=gap, k=k, missing=missing)
+        if self.moltype:
+            _alphabet_moltype_map[kalpha] = self.moltype
+        return kalpha
 
     @functools.singledispatchmethod
     def is_valid(self, seq: StrORBytesORArray) -> bool:
@@ -855,7 +862,7 @@ def deserialise_kmer_alphabet(data: dict) -> KmerAlphabet:
     return KmerAlphabet.from_rich_dict(data)
 
 
-class CodonAlphabet(tuple):
+class CodonAlphabet(tuple, AlphabetABC):
     """represents the sense-codons of a GeneticCode"""
 
     def __new__(
@@ -897,6 +904,8 @@ class CodonAlphabet(tuple):
         self._to_indices = {codon: i for i, codon in enumerate(self)}
         self._from_indices = {i: codon for codon, i in self._to_indices.items()}
         self.motif_length = 3
+        if monomers.moltype:
+            _alphabet_moltype_map[self] = monomers.moltype
 
     @property
     def gap_char(self):
@@ -1012,8 +1021,8 @@ def make_alphabet(*, chars, gap, missing, moltype):
 
     Notes
     -----
-    The moltype is associated with the alphabet, available as an
-    alphabet property.
+    The moltype is associated with the alphabet, available as the
+    alphabet.moltype property.
     """
     alpha = CharAlphabet(chars, gap=gap, missing=missing)
     _alphabet_moltype_map[alpha] = moltype


Test patch:
diff --git a/tests/test_core/test_new_alphabet.py b/tests/test_core/test_new_alphabet.py
index efe81e9c0..7af05f64d 100644
--- a/tests/test_core/test_new_alphabet.py
+++ b/tests/test_core/test_new_alphabet.py
@@ -604,3 +604,21 @@ def test_codon_alphabet_serlialise_round_trip(calpha):
     assert isinstance(got, new_alphabet.CodonAlphabet)
     assert list(got) == list(calpha)
     assert calpha.to_index("TTC") == 1
+
+
+@pytest.fixture(params=new_moltype.DNA.iter_alphabets())
+def alpha(request):
+    return request.param
+
+
+def test_alphabet_moltype(alpha):
+    assert alpha.moltype is new_moltype.DNA
+
+
+def test_alpha_no_moltype():
+    alpha = new_alphabet.CharAlphabet(list("ACG"))
+    assert alpha.moltype is None
+
+
+def test_codon_alphabet_moltype(calpha):
+    assert calpha.moltype is new_moltype.DNA