Repository: modernatx/seqlike

Issue:
Standard keyword arguments for translate in .aa()
As implemented currently, the SeqLike method `aa` passes kwargs to `SeqRecord.translate`, and so the default call to `.aa()` loses SeqRecord attributes like `id` and `name`.  I propose that we keep `SeqLike.translate(**kwargs)`, but remove these optional arguments from `aa` and make `.translate` return a new SeqRecord with the input SeqRecord attributes.

Discussion:
ericmjl: @ndousis, I think there is an issue with the CI, which will take a bit of time for me to get right tomorrow. Hope you don't mind the delay here.
ndousis: > @ndousis, I think there is an issue with the CI, which will take a bit of time for me to get right tomorrow. Hope you don't mind the delay here.

No rush

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 075e878..f95a2df 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,8 @@
 
 ## [Unreleased]
 
+-   Simplify `.aa()` interface, return original SeqRecord attributes (@ndousis)
+
 ## [v1.1.5] - 2021-11-15
 
 -   Ensure that `python-codon-tables` is listed as an explicit dependency in `setup.py` (@ericmjl)
diff --git a/environment.yml b/environment.yml
index 218fcc2..95ccb7a 100644
--- a/environment.yml
+++ b/environment.yml
@@ -3,7 +3,8 @@ channels:
   - conda-forge
   - bioconda
 dependencies:
-  - python>=3.9
+  - python<=3.9 # 18 Nov 2021: A super esoteric issue leads to us pinning env at 3.9.
+  # For more information, see: https://github.com/conda/conda-pack/issues/196#issuecomment-971790933
   - pip
   - scikit-learn
   - biopython
@@ -23,6 +24,7 @@ dependencies:
   - matplotlib
   - bokeh
   - python-codon-tables
+  - mkdocstrings
   # Code quality
   - pre-commit
   - darglint
@@ -32,5 +34,10 @@ dependencies:
   - mkdocstrings
   # Infrastructural
   - bump2version
+  # Dependencies of mknotebooks
+  - gitpython
+  - jupyter
+  - markdown
+  - nbconvert
   - pip:
-      - mknotebooks  # for notebooks in docs
+      - mknotebooks # for notebooks in docs
diff --git a/seqlike/SeqLike.py b/seqlike/SeqLike.py
index 9a6ab64..f452e6d 100644
--- a/seqlike/SeqLike.py
+++ b/seqlike/SeqLike.py
@@ -184,17 +184,31 @@ class SeqLike:
         The method will automagically translate the `._nt_record`
         of its SeqLike object if the `._aa_record` does not exist.
 
+        We preserve all NT record attributes for convenience in sequence manipulation
+        tasks where the starter sequence is an NT and we merely want the AA form.
+        The default behaviour, thus, has `id=True`, `name=True`, `description=True`,
+        `annotations=True`, and `dbxrefs=True`.
+
+        To change the behaviour,
+        you can set any of those to False in `.aa()`, for example:
+
+        ```python
+        seq.aa(id=False)
+        ```
+
         :param auto_translate: Whether to automagically translate
             an NT sequence into an AA sequence.
             Defaults to True.
-        :param kwargs: These kwargs are passed into
-            BioPython SeqRecord's `.translate()` method.
-            They can be used to set SeqRecord properties.
+        :param kwargs: These kwargs are passed into BioPython SeqRecord's
+            `.translate()` method.  The default behaviour is set to `id=True`,
+            `name=True`, `description=True`, `annotations=True`, and `dbxrefs=True`.
         :returns: copy of self with sequence object as amino acid sequence.
         """
         # Start with auto-translation
         if self._nt_record and self._aa_record is None and auto_translate:
-            return self.translate(**kwargs)
+            translate_kwargs = dict(id=True, name=True, description=True, annotations=True, dbxrefs=True)
+            translate_kwargs.update(kwargs)
+            return self.translate(**translate_kwargs)
 
         # Return based on _type.
         if self._type == "AA":


Test patch:
diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml
index cb5ce34..9c08093 100644
--- a/.github/workflows/tests.yaml
+++ b/.github/workflows/tests.yaml
@@ -28,14 +28,18 @@ jobs:
             seqlike-dev.tar.gz
           key: ${{ runner.os }}-env.${{ hashFiles('environment.yml') }}
 
+      - name: Install mamba
+        if: steps.cache-environment.outputs.cache-hit != 'true'
+        run: conda install -c conda-forge mamba
+
       - name: Build environment
         if: steps.cache-environment.outputs.cache-hit != 'true'
         run: |
-          conda env create -f environment.yml
+          mamba env update -f environment.yml
 
       - name: Install conda-pack
         if: steps.cache-environment.outputs.cache-hit != 'true'
-        run: conda install -c conda-forge conda-pack
+        run: mamba install -c conda-forge conda-pack
 
       - name: Run conda-pack
         if: steps.cache-environment.outputs.cache-hit != 'true'
diff --git a/tests/test_SeqLike.py b/tests/test_SeqLike.py
index 2508958..d7097f8 100644
--- a/tests/test_SeqLike.py
+++ b/tests/test_SeqLike.py
@@ -111,25 +111,34 @@ def test_SeqLike_interconversion():
     Suggested TODO: Split out SeqLike interconversion into a parametrized interconversion
     based on target types.
     """
-    seq = "TCGCACACTGCA"
-
-    a1 = SeqLike(seq, "dna").nt().to_str()
-    a2 = SeqLike(seq, "dna").aa().to_str()
+    seqstr = "TCGCACACTGCA"
+    seq0 = SeqLike(seqstr, "nt", id="id", name="name", description="description",
+                   annotations={"molecule_type": "DNA"}, dbxrefs=["/accessions=['Z78439']"])
+
+    seq1 = SeqLike(seq0, "dna").nt()
+    seq2 = SeqLike(seq0, "dna").aa()
+    seq3 = SeqLike(seq0, "dna").aa().nt()
+    assert_matched_properties(seq1, seq0)
+    assert_matched_properties(seq2, seq0)
+    assert_matched_properties(seq3, seq0)
+
+    a1 = SeqLike(seqstr, "dna").nt().to_str()
+    a2 = SeqLike(seqstr, "dna").aa().to_str()
     assert isinstance(a1, str)
     assert isinstance(a2, str)
 
-    b1 = SeqLike(seq, "dna").nt().to_seq()
-    b2 = SeqLike(seq, "dna").aa().to_seq()
+    b1 = SeqLike(seqstr, "dna").nt().to_seq()
+    b2 = SeqLike(seqstr, "dna").aa().to_seq()
     assert isinstance(b1, Seq)
     assert isinstance(b2, Seq)
 
-    c1 = SeqLike(seq, "dna").nt().to_seqrecord()
-    c2 = SeqLike(seq, "dna").aa().to_seqrecord()
+    c1 = SeqLike(seq0, "dna").nt().to_seqrecord()
+    c2 = SeqLike(seq0, "dna").aa().to_seqrecord()
     assert isinstance(c1, SeqRecord)
     assert isinstance(c2, SeqRecord)
+    assert_matched_properties(c1, seq0)
+    assert_matched_properties(c2, seq0)
 
-    seq1 = SeqLike(seq, "dna").nt()
-    seq2 = SeqLike(seq, "dna").aa()
     d1 = seq1.to_onehot()
     d2 = seq2.to_onehot()
     assert isinstance(d1, np.ndarray)
@@ -189,6 +198,17 @@ def test_backtranslation_overriding_codon_map(codon_table, sequence_type_and_alp
     assert_back_translate_properties(s)
 
 
+def assert_matched_properties(s1, s2):
+    """Test that SeqRecord-like sequence-independent attributes match."""
+    # retains SeqRecord-like metadata, e.g., `id` and `description`
+    assert s1.id == s2.id
+    assert s1.name == s2.name
+    assert s1.description == s2.description
+    # SeqRecord automatically sets the molecule_type annotation, so just check keys
+    assert s1.annotations.keys() == s2.annotations.keys()
+    assert s1.dbxrefs == s2.dbxrefs
+
+
 def assert_back_translate_properties(s):
     """Test for back_translate properties.
 
@@ -196,11 +216,7 @@ def assert_back_translate_properties(s):
     so we have refactored them out into a separate function.
     """
     assert isinstance(s.back_translate(), SeqLike)
-    # retains SeqLike metadata like `id` and `description`
-    assert s.back_translate().id == s.id
-    assert s.back_translate().description == s.description
-    assert s.back_translate().annotations == s.annotations
-    assert s.back_translate().dbxrefs == s.dbxrefs
+    assert_matched_properties(s.back_translate(), s)
     # but letter-specific metadata is changed
     assert s.back_translate().letter_annotations != s.letter_annotations