Repository: cogent3/cogent3

Issue:
modify SeqData
- [ ] alphabet must be provided, drop need for the moltype argument
- [ ] add private attribute for `_seq_maker` (it's `Optional[type]`) and setter / getter properties called `seq_maker`
- [ ] modify `__getitem__` so current behaviour (returning `SeqDataView`) occurs if `self._seq_maker is None`
- [ ] `__getitem__` returns self.seq_maker(`SeqDataView`, name) otherwise

Discussion:
@fredjaya: Also
- [x] add slots to `SeqData` (and `AlignedData`)@fredjaya: moltype dropped and slots added in 0dbf437 @fredjaya: @GavinHuttley wanting to clarify the intended output and usage of  `seq_maker`.

For example, to return a `cogent3.core.sequence.Sequence`:
```python
data = {"s1": "ACTG", "s2": "ACTGT"}
alpha = get_moltype("dna").alphabets.degen_gapped

# seq_maker can be defined upon construction
seqdata = SeqData(data, alphabet=alpha, seq_maker=Sequence)

# alternatively with the setter
seqdata2 = SeqData(data, alphabet=alpha)
seqdata2.seq_maker = Sequence
```

Should `seqdata["s1"]` be returning the output of `Sequence(seq=data["s1"], name="s1")`?@GavinHuttley: From your example above, yes. But I would envision assigning the method from the moltype instance, e.g.
```python
data = {"s1": "ACTG", "s2": "ACTGT"}
dna = get_moltype("dna")

# seq_maker can be defined upon construction
seqdata = SeqData(data, alphabet=dna.alphabets.degen_gapped, seq_maker=dna.make_seq)

# alternatively with the setter
seqdata2 = SeqData(data, alphabet=alpha)
seqdata2.seq_maker = dna.make_seq
```@GavinHuttley: @fredjaya let me qualify, instead of `data["s1"]`, it will be a `SeqDataView(seqid="s1", seq=<the SeqData instance creating the view>)` instance.@fredjaya: - [x] Without `self.seq_maker`, use `__getitem__` to create a `SeqDataView` instance of the correct item.
- [x] With `self.seq_maker`, create `seq_maker(seq=SeqDataView, name=seqid)`, where `SeqDataView` takes:
    - `seq = self`
    - `seq_len = len(self._data[seqid])`
    - `seqid`@fredjaya: @GavinHuttley aside from `MolType.make_seq`, are there any other specific methods/types that should be accepted? @GavinHuttley: The only requirement we should have on the value provided to `seq_maker` is that it's callable. We document that we will invoke the callable with keyword arguments for `seq` and `name`.@fredjaya: If I understand correctly, `make_seq` (and likely other callables) will return `AttributeError: 'SeqData' object has no attribute 'replace'` if a `SeqDataView` instance is passed as `seq`?  In the example of:
```python
data = {"s1": "ACTG", "s2": "ACTGT"}
dna = get_moltype("dna")
seqdata = SeqData(data, alphabet=dna.alphabets.degen_gapped, seq_maker=dna.make_seq)
```

And the `SeqData` method:
```python
def _make_seq(self, seqid): # called in __getitem__
    seqdataview = self.get_seq_view(seqid=seqid)
    if self._seq_maker is None:
        return seqdataview
    return self._seq_maker(seq=seqdataview, name=seqid)
```

Feel like i'm still missing a key piece of understanding `seq_maker`!@GavinHuttley: That error is happening because the current implementation of `MolType.make_seq()` does not know about `SeqView` yet. So for now, raise an issue about that and in your tests directly use `<moltype_instance_make_seq>`. That latter just points to the class, which is what you were doing before.

Solution patch:
diff --git a/src/cogent3/core/new_alignment.py b/src/cogent3/core/new_alignment.py
index b0ece4bca..12c037a79 100644
--- a/src/cogent3/core/new_alignment.py
+++ b/src/cogent3/core/new_alignment.py
@@ -645,6 +645,7 @@ class SequenceCollection:
             "info": self.info.copy(),
             "annotation_db": self.annotation_db,
             "is_reversed": self._is_reversed,
+            "source": self.source,
         }
 
     @property
@@ -2512,17 +2513,12 @@ def make_unaligned_seqs(
     # refactor: design
     # rename offset to offsets as it could track potentially multiple offsets
 
-    # refactor: design
-    # define a source attribute rather than storing as .info["source"]
-    # this will also replace the previous name attribute
-
     # refactor: design
     # currently reversal can only be applied to the entire collection.
     # This should be made more flexible.
 
     moltype = new_moltype.get_moltype(moltype)
     alphabet = moltype.most_degen_alphabet()
-
     if len(data) == 0:
         raise ValueError("data must be at least one sequence.")
 
@@ -2591,7 +2587,8 @@ def _(
         raise ValueError(f"Setting offset is not supported for {data=}")
 
     info = info if isinstance(info, dict) else {}
-    info["source"] = str(source) if source else str(info.get("source", "unknown"))
+    source = str(source) if source else str(info.get("source", "unknown"))
+    info["source"] = source
     seqs = SequenceCollection(
         seqs_data=data,
         moltype=moltype,
@@ -4054,6 +4051,7 @@ class Alignment(SequenceCollection):
             "info": self.info.copy(),
             "annotation_db": self.annotation_db,
             "slice_record": self._slice_record,
+            "source": self.source,
         }
 
     @singledispatchmethod
@@ -6552,7 +6550,8 @@ def _(
         raise ValueError(f"Setting offset is not supported for {data=}")
 
     info = info if isinstance(info, dict) else {}
-    info["source"] = str(source) if source else str(info.get("source", "unknown"))
+    source = str(source) if source else str(info.get("source", "unknown"))
+    info["source"] = source
 
     sr = (
         new_sequence.SliceRecord(parent_len=data.align_len, step=-1)


Test patch:
diff --git a/tests/test_core/test_new_alignment.py b/tests/test_core/test_new_alignment.py
index 7f0783453..34e3bc669 100644
--- a/tests/test_core/test_new_alignment.py
+++ b/tests/test_core/test_new_alignment.py
@@ -726,11 +726,11 @@ def test_sequence_collection_init_ordered(ordered1, ordered2):
 
 
 @pytest.mark.parametrize("load_cls", [load_aligned_seqs, load_unaligned_seqs])
-def test_sequence_collection_info_source(load_cls):
-    """info.source exists if load seqs given a filename"""
+def test_sequence_collection_source(load_cls):
+    """.source exists if load seqs given a filename"""
     path = pathlib.Path("data/brca1.fasta")
     seqs = load_cls(path, moltype="dna", new_type=True)
-    assert seqs.info.source == str(path)
+    assert seqs.source == str(path)
 
 
 @pytest.mark.parametrize(
@@ -2561,6 +2561,7 @@ def test_sequence_collection_to_rich_dict():
             "moltype": seqs.moltype.label,
             "name_map": seqs._name_map,
             "info": seqs.info,
+            "source": "unknown",
         },
     }
     assert got == expect
@@ -2578,6 +2579,7 @@ def test_sequence_collection_to_rich_dict_reversed_seqs():
             "moltype": seqs.moltype.label,
             "name_map": seqs._name_map,
             "info": seqs.info,
+            "source": "unknown",
         },
         "type": get_object_provenance(seqs),
         "version": __version__,
@@ -5536,3 +5538,15 @@ def test_coerce_moltype_obj(mk_cls):
     )
     coll = mk_cls(data, moltype="text")
     coll = coll.to_moltype("rna")
+
+
+@pytest.mark.parametrize(
+    "mk_cls",
+    [load_aligned_seqs, load_unaligned_seqs],
+)
+def test_source_propagates(mk_cls, DATA_DIR):
+    fn = DATA_DIR / "brca1.fasta"
+    coll = mk_cls(fn, moltype="dna", new_type=True)
+    assert coll.source == str(fn)
+    subcoll = coll.take_seqs(["Human", "Chimpanzee"])
+    assert subcoll.source == str(fn)