Repository: cogent3/cogent3
Pull request #1881 (2024-05-30 05:31:30)

Start on SequenceCollections refactor
working towards #1867

questions wrt direction are commented in place 

coveralls: ## Pull Request Test Coverage Report for [Build 9297525509](https://coveralls.io/builds/67784159)


### Details

* **47** of **51**   **(92.16%)**  changed or added relevant lines in **2** files are covered.
* No unchanged relevant lines lost coverage.
* Overall coverage increased (+**0.03%**) to **92.013%**

---

|  Changes Missing Coverage | Covered Lines | Changed/Added Lines | % |
| :-----|--------------|--------|---: |
| [src/cogent3/core/new_alignment.py](https://coveralls.io/builds/67784159/source?filename=src%2Fcogent3%2Fcore%2Fnew_alignment.py#L441) | 45 | 46 | 97.83%
| [src/cogent3/__init__.py](https://coveralls.io/builds/67784159/source?filename=src%2Fcogent3%2F__init__.py#L144) | 2 | 5 | 40.0%
<!-- | **Total:** | **47** | **51** | **92.16%** | -->


|  Totals | [![Coverage Status](https://coveralls.io/builds/67784159/badge)](https://coveralls.io/builds/67784159) |
| :-- | --: |
| Change from base [Build 9281005365](https://coveralls.io/builds/67758215): |  0.03% |
| Covered Lines: | 30539 |
| Relevant Lines: | 33190 |

---
##### 💛  - [Coveralls](https://coveralls.io)


Patch:
diff --git a/src/cogent3/__init__.py b/src/cogent3/__init__.py
index 7cd610989..eacf4e790 100644
--- a/src/cogent3/__init__.py
+++ b/src/cogent3/__init__.py
@@ -120,7 +120,7 @@ def _make_seq_container(
 
 
 def make_unaligned_seqs(
-    data, moltype=None, label_to_name=None, info=None, source=None, **kw
+    data, moltype=None, label_to_name=None, info=None, source=None, new_type=False, **kw
 ):
     """Initialize an unaligned collection of sequences.
 
@@ -140,7 +140,19 @@ def make_unaligned_seqs(
     **kw
         other keyword arguments passed to SequenceCollection
     """
+    if new_type and moltype is None:
+        raise ValueError("Argument 'moltype' is required when 'new_type=True'")
+    elif new_type:
+        from cogent3.core import new_alignment
 
+        return new_alignment.make_unaligned_seqs(
+            data=data,
+            moltype=moltype,
+            label_to_name=label_to_name,
+            info=info,
+            source=source,
+            **kw,
+        )
     return _make_seq_container(
         SequenceCollection,
         data,
diff --git a/src/cogent3/core/new_alignment.py b/src/cogent3/core/new_alignment.py
index 136899823..f548ec53e 100644
--- a/src/cogent3/core/new_alignment.py
+++ b/src/cogent3/core/new_alignment.py
@@ -1,102 +1,46 @@
 from __future__ import annotations
 
+import re
+import typing
+
+from collections import defaultdict
 from dataclasses import InitVar, dataclass, field
 from functools import singledispatch, singledispatchmethod
-from typing import Callable, Iterator, Optional, Union
+from pathlib import Path
+from typing import Callable, Iterator, Mapping, Optional, Union
 
 import numpy
 
-from cogent3 import get_moltype
-from cogent3._version import __version__
-from cogent3.core.alphabet import CharAlphabet
-from cogent3.core.location import IndelMap
-from cogent3.core.moltype import MolType
-from cogent3.core.sequence import (
-    Sequence,
-    SliceRecordABC,
-    _input_vals_neg_step,
-    _input_vals_pos_step,
-)
-from cogent3.util.misc import get_object_provenance
-
-
-SeqTypes = Union[str, bytes, numpy.ndarray]
-
-
-@singledispatch
-def seq_index(seq: SeqTypes, alphabet: CharAlphabet) -> numpy.ndarray:
-    raise NotImplementedError(
-        f"{seq_index.__name__} not implemented for type {type(seq)}"
-    )
-
+import cogent3.core.new_alphabet as new_alpha
+import cogent3.core.new_moltype as new_moltype
+import cogent3.core.new_sequence as new_seq
 
-@seq_index.register
-def _(seq: str, alphabet: CharAlphabet) -> numpy.ndarray:
-    return alphabet.to_indices(seq)
-
-
-@seq_index.register
-def _(seq: bytes, alphabet: CharAlphabet) -> numpy.ndarray:
-    return alphabet.to_indices(seq)
-
-
-@seq_index.register
-def _(seq: numpy.ndarray, alphabet: CharAlphabet) -> numpy.ndarray:
-    return seq.astype(alphabet.array_type)
-
-
-@singledispatch
-def validate_names(correct_names: Union[dict, tuple, list], names: tuple) -> tuple:
-    """
-    Helper to check that names passed as args match sequence names.
-    dict (data) for constructor; tuple for SeqData instance
-    """
-    raise NotImplementedError(
-        f"validate_names not implemented for type {type(correct_names)}"
-    )
-
-
-@validate_names.register
-def _(correct_names: dict, names: tuple) -> tuple:
-    keys = correct_names.keys()
-    if names is None:
-        return tuple(keys)
-    if set(names) == set(keys):
-        return names
-    raise ValueError("names do not match dictionary keys")
-
-
-def _names_list_tuple(correct_names: Union[tuple, list], names: tuple):
-    """List and tuples have the same implementation for dispatch"""
-    if names is None:
-        return correct_names
-    if set(names) <= set(correct_names):
-        return tuple(names)
-    raise ValueError("some names do not match")
+from cogent3.core.info import Info as InfoClass
+from cogent3.core.location import IndelMap
+from cogent3.util import warning as c3warn
+from cogent3.util.misc import get_setting_from_environ
 
 
-@validate_names.register
-def _(correct_names: tuple, names: tuple) -> tuple:
-    return _names_list_tuple(correct_names, names)
+PrimitiveSeqTypes = Union[str, bytes, numpy.ndarray]
 
 
-@validate_names.register
-def _(correct_names: list, names: tuple) -> tuple:
-    return _names_list_tuple(correct_names, names)
+def assign_sequential_names(num_seqs: int, base_name: str = "seq", start_at: int = 0):
+    """Returns list of num_seqs sequential, unique names."""
+    return [f"{base_name}_{i}" for i in range(start_at, start_at + num_seqs)]
 
 
-class SeqDataView(SliceRecordABC):
+class SeqDataView(new_seq.SliceRecordABC):
     """
-    A view class for SeqData, providing properties for different
+    A view class for SeqsData, providing properties for different
     representations.
 
-    self.seq is a SeqData() instance, but other properties are a reference to a
+    self.seq is a SeqsData() instance, but other properties are a reference to a
     single seqid only.
 
     Example
     -------
     data = {"seq1": "ACGT", "seq2": "GTTTGCA"}
-    sd = SeqData(data=data)
+    sd = SeqsData(data=data)
     sdv = sd.get_seq_view(seqid="seq1")
     """
 
@@ -105,20 +49,22 @@ class SeqDataView(SliceRecordABC):
     def __init__(
         self,
         *,
-        seq: SeqData,
+        seq: SeqsData,
+        seq_len: int,
         start: Optional[int] = None,
         stop: Optional[int] = None,
         step: Optional[int] = None,
         offset: int = 0,
         seqid: Optional[str] = None,
-        seq_len: Optional[int] = None,
     ):
         if step == 0:
             raise ValueError("step cannot be 0")
         step = 1 if step is None else step
 
         self._seq_len = self._checked_seq_len(seq_len)
-        func = _input_vals_pos_step if step > 0 else _input_vals_neg_step
+        func = (
+            new_seq._input_vals_pos_step if step > 0 else new_seq._input_vals_neg_step
+        )
         start, stop, step = func(self._seq_len, start, stop, step)
         self.seq = seq
         self.start = start
@@ -186,94 +132,54 @@ class SeqDataView(SliceRecordABC):
             f"seqid={self.seqid!r}, seq_len={self.seq_len})"
         )
 
-    def to_rich_dict(self):
-        # get the current state
-        data = {"type": get_object_provenance(self), "version": __version__}
-        data["init_args"] = self._get_init_kwargs()
-        # since we will truncate the seq, we don't need start, stop,
-        # step is sufficient
-        data["init_args"]["step"] = self.step
-        if self.is_reversed:
-            adj = self.seq_len + 1
-            start, stop = self.stop + adj, self.start + adj
-        else:
-            start, stop = self.start, self.stop
-
-        data["init_args"]["seq"] = str(self[start:stop])
-        data["init_args"]["offset"] = int(self.parent_start)
-        return data
-
-    @classmethod
-    def from_rich_dict(cls, data: dict):
-        init_args = data.pop("init_args")
-        if "offset" in data:
-            init_args["offset"] = data.pop("offset")
-        return cls(**init_args)
-
+    # todo: do we support copy? do we support copy with sliced?
     def copy(self, sliced: bool = False):
-        """returns copy
-
-        Parameters
-        ----------
-        sliced
-            if True, the underlying sequence is truncated and the start/stop
-            adjusted
-        """
-        if not sliced:
-            return self.__class__(
-                seq=self.seq,
-                start=self.start,
-                stop=self.stop,
-                step=self.step,
-                offset=self.offset,
-                seqid=self.seqid,
-                seq_len=self.seq_len,
-            )
-        return self.from_rich_dict(self.to_rich_dict())
+        """returns copy"""
+        return self
 
 
-class SeqData:
+class SeqsData:
     __slots__ = ("_data", "_alphabet", "_names", "_make_seq")
 
     def __init__(
         self,
-        data: dict[str, SeqTypes],
-        alphabet: CharAlphabet,
-        names: Optional[Union[tuple[str], None]] = None,
-        make_seq: Optional[type] = None,
+        data: dict[str, PrimitiveSeqTypes],
+        alphabet: new_alpha.CharAlphabet,
+        make_seq: Callable = None,
     ):
         self._alphabet = alphabet
-        self._names = validate_names(data, names)
         self._make_seq = make_seq
-        # convert from string to array of uint8
         self._data: dict[str, numpy.ndarray] = {
-            k: seq_index(v, self._alphabet) for k, v in data.items()
+            str(name): self._alphabet.to_indices(seq) for name, seq in data.items()
         }
 
     @property
-    def names(self) -> Iterator[str]:
-        yield from self._names
+    def names(self) -> list:
+        return list(self._data.keys())
 
     @property
-    def make_seq(self) -> Union[SeqDataView, Sequence]:
+    def make_seq(self) -> Union[SeqDataView, new_seq.Sequence]:
+        """if set, returns a function that takes 'seq' and 'name' as keyword
+        arguments and returns a given Sequence from the collection.
+
+        Notes
+        -----
+        Can be set with any callable function that takes 'seq' and 'name' as
+        keyword arguments. Typically set with '<moltype-instance>.make_seq'.
+        """
         return self._make_seq
 
     @make_seq.setter
     def make_seq(self, make_seq: Callable) -> None:
         self._make_seq = make_seq
 
-    @singledispatchmethod
-    def __getitem__(self, index: Union[str, int]) -> SeqDataView:
-        raise NotImplementedError(f"__getitem__ not implemented for {type(index)}")
-
-    @__getitem__.register
-    def _(self, index: str) -> SeqDataView:
-        sdv = self.get_seq_view(seqid=index)
-        return sdv if self._make_seq is None else self.make_seq(sdv, seqid=index)
+    @property
+    def num_seqs(self) -> int:
+        return len(self._data)
 
-    @__getitem__.register
-    def _(self, index: int) -> SeqDataView:
-        return self[self._names[index]]
+    @property
+    def alphabet(self) -> new_alpha.CharAlphabet:
+        return self._alphabet
 
     def get_seq_array(
         self, *, seqid: str, start: int = None, stop: int = None
@@ -294,10 +200,387 @@ class SeqData:
         seq_len = len(self._data[seqid])
         return SeqDataView(seq=self, seqid=seqid, seq_len=seq_len)
 
+    def __len__(self):
+        return self.num_seqs
+
+    @singledispatchmethod
+    def __getitem__(
+        self, index: Union[str, int]
+    ) -> Union[SeqDataView, new_seq.Sequence]:
+        raise NotImplementedError(f"__getitem__ not implemented for {type(index)}")
+
+    @__getitem__.register
+    def _(self, index: str) -> Union[SeqDataView, new_seq.Sequence]:
+        sdv = self.get_seq_view(seqid=index)
+        return (
+            sdv
+            if self.make_seq is None
+            else self.make_seq(seq=sdv.str_value, name=index)
+        )
+
+    @__getitem__.register
+    def _(self, index: int) -> Union[SeqDataView, new_seq.Sequence]:
+        return self[self.names[index]]
+
+
+@singledispatch
+def prep_for_seqs_data(
+    data: Union[dict, SeqsData], moltype: new_moltype.MolType
+) -> dict[str, numpy.ndarray]:
+    raise NotImplementedError(f"SeqsData can not be constructed for type {type(data)}")
+
+
+@prep_for_seqs_data.register
+def _(data: SeqsData, moltype: new_moltype.MolType) -> dict[str, numpy.ndarray]:
+    if moltype.is_compatible_alphabet(data.alphabet):
+        return data
+    else:
+        raise ValueError(
+            f"Provided moltype: {moltype} is not compatible with SeqsData alphabet {data.alphabet}"
+        )
+
+
+@prep_for_seqs_data.register
+def _(data: dict, moltype: new_moltype.MolType) -> dict[str, numpy.ndarray]:
+    # todo: kath, when to use .alphabet and when to use .degen_gapped_alphabet?
+    alpha = moltype.alphabet
+    seqs_data = SeqsData(data=data, alphabet=alpha)
+    return prep_for_seqs_data(seqs_data, moltype)
+
+
+@prep_for_seqs_data.register
+def _(data: list, moltype: new_moltype.MolType) -> dict[str, numpy.ndarray]:
+    names = assign_sequential_names(len(data))
+    named_data = dict(zip(names, data))
+    return prep_for_seqs_data(named_data, moltype)
+
+
+def make_unaligned_seqs(
+    *,
+    data: Union[dict[str, PrimitiveSeqTypes], SeqsData, list],
+    moltype: Union[str, new_moltype.MolType],
+    label_to_name: Callable = None,
+    info: dict = None,
+    source: Union[str, Path] = None,
+    **kwargs,
+):
+    """Initialize an unaligned collection of sequences.
+
+    Parameters
+    ----------
+    data
+        sequences, assumes a dict {name: seq, ...} or a SeqsData
+    moltype
+        string representation of the moltype, e.g., 'dna', 'protein'.
+    label_to_name
+        function for converting original names into other names.
+    info
+        a dict from which to make an info object
+    source
+        origins of this data, defaults to 'unknown'. Converted to a string
+        and added to info["source"].
+    **kwargs
+        other keyword arguments to be passed to SequenceCollection
+    """
+    moltype = new_moltype.get_moltype(moltype)
+
+    if len(data) == 0:
+        raise ValueError("data must be at least one sequence.")
+    seqs_data = prep_for_seqs_data(data, moltype)
+
+    return SequenceCollection(seqs_data=seqs_data, moltype=moltype, info=info)
+
+
+class SequenceCollection:
+    def __init__(
+        self,
+        *,
+        seqs_data: SeqsData,
+        moltype: new_moltype.MolType,
+        info: Union[dict, InfoClass] = None,
+    ):
+        self._seqs_data = seqs_data
+        self.moltype = moltype
+        self._names = self._seqs_data.names
+        self._seqs_data.make_seq = self.moltype.make_seq
+        if not isinstance(info, InfoClass):
+            info = InfoClass(info) if info else InfoClass()
+        self.info = info
+        self._repr_policy = dict(num_seqs=10, num_pos=60, ref_name="longest", wrap=60)
+
+    @property
+    def names(self) -> Iterator[str]:
+        return self._names
+
+    @property
+    def seqs(self) -> SeqsData:
+        return self._seqs_data
+
+    def iter_seqs(
+        self, seq_order: list = None
+    ) -> Iterator[Union[new_seq.Sequence, SeqsData]]:
+        """Iterates over sequences in the collection, in order.
+
+        Parameters
+        ----------
+        seq_order:
+            list of seqids giving the order in which seqs will be returned.
+            Defaults to self.names
+        """
+
+        seqs = self.seqs
+        get = seqs.__getitem__
+        for key in seq_order or self.names:
+            yield get(key)
+
+    @property
+    def num_seqs(self) -> int:
+        return self._seqs_data.num_seqs
+
+    @property
+    @c3warn.deprecated_callable(
+        version="2025.5", reason=".seqs can now be indexed by name", new=".seqs"
+    )
+    def named_seqs(self) -> SeqsData:  # pragma: no cover
+        return self.seqs
+
+    def take_seqs(
+        self, names: Union[str, typing.Sequence[str]], negate: bool = False, **kwargs
+    ):
+        """Returns new collection containing only specified seqs.
+
+        Parameters
+        ----------
+        names
+            sequences to select (or exclude if negate=True)
+        negate
+            select all sequences EXCEPT names
+        kwargs
+            keyword arguments to be passed to the constructor of the new seqcollection
+        """
+
+        # todo: kath, add subset method onto SeqsData
+        if negate:
+            selected_seqs = {
+                name: self.seqs.get_seq_str(seqid=name)
+                for name in self.names
+                if name not in names
+            }
+        else:
+            selected_seqs = {
+                name: self.seqs.get_seq_str(seqid=name)
+                for name in names
+                if name in self.names
+            }
+
+        if not selected_seqs:
+            return {}
+
+        moltype = kwargs.pop("moltype", self.moltype)
+
+        seqs_data = SeqsData(data=selected_seqs, alphabet=moltype.alphabet)
+        result = self.__class__(
+            seqs_data=seqs_data, moltype=moltype, info=self.info, **kwargs
+        )
+        # todo: kath, update annotation_db and assign to result when we have annotations working
+        return result
+
+    def __repr__(self):
+        seqs = []
+        limit = 10
+        delimiter = ""
+
+        repr_seq_names = [min(self.names, key=lambda name: len(self.seqs[name]))]
+        if len(self.names) > 1:
+            # In case of a tie, min and max return first.
+            # reversed ensures if all seqs are of same length, different seqs are returned
+            repr_seq_names.append(
+                max(reversed(self.names), key=lambda name: len(self.seqs[name]))
+            )
+
+        for name in repr_seq_names:
+            elts = list(str(self.seqs[name])[: limit + 1])
+            if len(elts) > limit:
+                elts[-1] = "..."
+            seqs.append(f"{name}[{delimiter.join(elts)}]")
+
+        if len(self.names) > 2:
+            seqs.insert(1, "...")
+
+        seqs = ", ".join(seqs)
+
+        return f"{len(self.names)}x ({seqs}) {self.moltype.label} seqcollection"
+
+    def _repr_html_(self) -> str:
+        settings = self._repr_policy.copy()
+        env_vals = get_setting_from_environ(
+            "COGENT3_ALIGNMENT_REPR_POLICY",
+            dict(num_seqs=int, num_pos=int, wrap=int),
+        )
+        settings.update(env_vals)
+        return self.to_html(
+            name_order=self.names[: settings["num_seqs"]],
+            limit=settings["num_pos"],
+            wrap=settings["wrap"],
+        )
+
+    def to_html(
+        self,
+        name_order: Optional[typing.Sequence[str]] = None,
+        wrap: int = 60,
+        limit: Optional[int] = None,
+        colors: Optional[Mapping[str, str]] = None,
+        font_size: int = 12,
+        font_family: str = "Lucida Console",
+    ) -> str:
+        """returns html with embedded styles for sequence colouring
+
+        Parameters
+        ----------
+        name_order
+            order of names for display.
+        wrap
+            number of alignment columns per row
+        limit
+            truncate alignment to this length
+        colors
+            {character
+            moltype.
+        font_size
+            in points. Affects labels and sequence and line spacing
+            (proportional to value)
+        font_family
+            string denoting font family
+
+        Examples
+        ---------
+
+        In a jupyter notebook, this code is used to provide the representation.
+
+        .. code-block:: python
+
+            seq_col # is rendered by jupyter
+
+        You can directly use the result for display in a notebook as
+
+        .. code-block:: python
+
+            from IPython.core.display import HTML
+            HTML(seq_col.to_html())
+        """
+        css, styles = self.moltype.get_css_style(
+            colors=colors, font_size=font_size, font_family=font_family
+        )
+
+        seq_lengths = numpy.array([len(seq) for seq in self.seqs])
+        min_val = seq_lengths.min()
+        max_val = seq_lengths.max()
+        med_val = numpy.median(seq_lengths)
+
+        if name_order:
+            selected = self.take_seqs(name_order)
+        else:
+            name_order = self.names
+            selected = self
+
+        # Stylise each character in each sequence
+        gaps = "".join(selected.moltype.gaps)
+        template = '<span class="%s">%%s</span>'
+        styled_seqs = defaultdict(list)
+        max_truncated_len = 0
+        for name in name_order:
+            sequence = str(self.seqs[name])[:limit]
+            seq_len = len(sequence)
+            max_truncated_len = max(seq_len, max_truncated_len)
+            start_gap = re.search(f"^[{gaps}]+", sequence)
+            end_gap = re.search(f"[{gaps}]+$", sequence)
+            start = 0 if start_gap is None else start_gap.end()
+            end = seq_len if end_gap is None else end_gap.start()
+
+            seq = []
+            for i, char in enumerate(sequence):
+                if i < start or i >= end:
+                    style = f"terminal_ambig_{self.moltype.label}"
+                else:
+                    style = styles[char]
+                s = template % style
+                s = s % char
+                seq.append(s)
+
+            styled_seqs[name] = seq
+
+        # Ensure all sublists are of same length
+        for name in styled_seqs:
+            if len(styled_seqs[name]) < max_truncated_len:
+                styled_seqs[name].extend(
+                    [""] * (max_truncated_len - len(styled_seqs[name]))
+                )
+
+        # Make html table
+        seqs = numpy.array([styled_seqs[n] for n in name_order], dtype="O")
+        table = ["<table>"]
+        seq_ = "<td>%s</td>"
+        label_ = '<td class="label">%s</td>'
+        num_row_ = '<tr class="num_row"><td></td><td><b>{:,d}</b></td></tr>'
+        for i in range(0, max_truncated_len, wrap):
+            table.append(num_row_.format(i))
+            seqblock = seqs[:, i : i + wrap].tolist()
+            for n, s in zip(name_order, seqblock):
+                s = "".join(s)
+                # Filter out rows that are empty (due to combination of shorter sequences + wrapping)
+                if s != "":
+                    row = "".join([label_ % n, seq_ % s])
+                    table.append(f"<tr>{row}</tr>")
+        table.append("</table>")
+        if (
+            limit
+            and limit < len(selected)
+            or name_order
+            and len(name_order) < len(selected.names)
+        ):
+            summary = (
+                "%s x {min=%s, median=%s, max=%s} (truncated to %s x %s) %s sequence collection"
+            ) % (
+                self.num_seqs,
+                min_val,
+                med_val,
+                max_val,
+                len(name_order) if name_order else len(selected.names),
+                limit or len(selected),
+                selected.moltype.label,
+            )
+        else:
+            summary = ("%s x {min=%s, median=%s, max=%s} %s sequence collection") % (
+                self.num_seqs,
+                min_val,
+                med_val,
+                max_val,
+                selected.moltype.label,
+            )
+
+        text = [
+            "<style>",
+            ".c3align table {margin: 10px 0;}",
+            ".c3align td { border: none !important; text-align: left !important; }",
+            ".c3align tr:not(.num_row) td span {margin: 0 2px;}",
+            ".c3align tr:nth-child(even) {background: #f7f7f7;}",
+            ".c3align .num_row {background-color:rgba(161, 195, 209, 0.5) !important; border-top: solid 1px black; }",
+            ".c3align .label { font-size: %dpt ; text-align: right !important; "
+            "color: black !important; padding: 0 4px; display: table-cell !important; "
+            "font-weight: normal !important; }" % font_size,
+            "\n".join([".c3align " + style for style in css]),
+            "</style>",
+            '<div class="c3align">',
+            "\n".join(table),
+            f"<p><i>{summary}</i></p>",
+            "</div>",
+        ]
+        return "\n".join(text)
+
 
 @singledispatch
 def seq_to_gap_coords(
-    seq: Union[str, numpy.ndarray], moltype: MolType
+    seq: Union[str, numpy.ndarray], moltype: new_moltype.MolType
 ) -> tuple[str, IndelMap]:
     """
     Takes a sequence with (or without) gaps and returns an ungapped sequence
@@ -307,8 +590,8 @@ def seq_to_gap_coords(
 
 
 @seq_to_gap_coords.register
-def _(seq: str, moltype: MolType) -> tuple[str, IndelMap]:
-    seq = moltype.make_seq(seq)
+def _(seq: str, moltype: new_moltype.MolType) -> tuple[str, IndelMap]:
+    seq = moltype.make_seq(seq=seq)
     indel_map, ungapped_seq = seq.parse_out_gaps()
 
     if indel_map.num_gaps == 0:
@@ -318,8 +601,8 @@ def _(seq: str, moltype: MolType) -> tuple[str, IndelMap]:
 
 
 @seq_to_gap_coords.register
-def _(seq: numpy.ndarray, moltype: MolType) -> tuple[str, IndelMap]:
-    gap_char = moltype.alphabets.degen_gapped.index(moltype.gap)
+def _(seq: numpy.ndarray, moltype: new_moltype.MolType) -> tuple[str, IndelMap]:
+    gap_char = moltype.degen_gapped_alphabet.index(moltype.gap)
     # Create boolean array of gaps to get ungapped
     gaps_bool = seq == gap_char
     ungapped = seq[~gaps_bool]
@@ -369,27 +652,26 @@ class AlignedDataView(SeqDataView):
 
 @dataclass
 class AlignedData:
-    # Look out for any overlaps with SeqData
+    # Look out for any overlaps with SeqsData
     seqs: Optional[dict[str, numpy.ndarray]] = None
     gaps: Optional[dict[str, numpy.ndarray]] = None
-    _moltype: MolType = field(init=False)
+    _moltype: new_moltype.MolType = field(init=False)
     _names: tuple[str] = field(init=False)
-    _alpha: CharAlphabet = field(init=False)
+    _alpha: new_alpha.CharAlphabet = field(init=False)
     align_len: int = 0
     moltype: InitVar[Union[str, None]] = "dna"
     names: InitVar[Union[tuple[str], None]] = None
 
     def __post_init__(self, moltype, names):
-        self._moltype = get_moltype(moltype)
-        self._alpha = self._moltype.alphabets.degen_gapped
-        self._names = validate_names(self.seqs, names)
-        self.seqs = {k: seq_index(v, self._alpha) for k, v in self.seqs.items()}
+        self._moltype = new_moltype.get_moltype(moltype)
+        self._alpha = self._moltype.degen_gapped_alphabet
+        self.seqs = {k: self._alpha.to_indices(v) for k, v in self.seqs.items()}
 
     @classmethod
     def from_gapped_seqs(
         cls,
         data: dict[str, Union[str, numpy.ndarray]],
-        moltype: Union[str, MolType] = "dna",
+        moltype: Union[str, new_moltype.MolType] = "dna",
         names: Optional[tuple[str]] = None,
     ):
         """
@@ -400,14 +682,14 @@ class AlignedData:
             raise ValueError("All sequence lengths must be the same.")
 
         align_len = seq_lengths.pop()
-        moltype = get_moltype(moltype)
+        moltype = new_moltype.get_moltype(moltype)
 
         seqs = {}
         gaps = {}
         for name, seq in data.items():
             seqs[name], gaps[name] = seq_to_gap_coords(seq, moltype)
 
-        names = validate_names(seqs, names)
+        names = names or list(data.keys)
 
         return cls(
             seqs=seqs,


Test patch:
diff --git a/tests/test_core/test_alignment.py b/tests/test_core/test_alignment.py
index 3e0f4e8c1..acdc8caed 100644
--- a/tests/test_core/test_alignment.py
+++ b/tests/test_core/test_alignment.py
@@ -218,7 +218,7 @@ class SequenceCollectionBaseTests(object):
         self.assertTrue((un_list == first_list) or (un_list == sec_list))
         self.assertNotEqual(first_list, sec_list)
 
-    def test_iter_seqs(self):
+    def test_iter_seqs(self):  # ported
         """SequenceCollection iter_seqs() method should support reordering of seqs"""
         self.ragged_padded = self.Class(
             self.ragged_padded.named_seqs, names=["a", "b", "c"]
@@ -238,7 +238,7 @@ class SequenceCollectionBaseTests(object):
         self.assertEqual(list(self.ordered1.iter_selected()), ["A"] * 5 + ["B"] * 5)
         self.assertEqual(list(self.ordered2.iter_selected()), ["B"] * 5 + ["A"] * 5)
 
-    def test_take_seqs(self):
+    def test_take_seqs(self):  # ported
         """SequenceCollection take_seqs should return new SequenceCollection with selected seqs."""
         a = self.ragged_padded.take_seqs(list("bc"))
         self.assertTrue(isinstance(a, _SequenceCollectionBase))
@@ -247,7 +247,7 @@ class SequenceCollectionBaseTests(object):
         a = self.ragged_padded.take_seqs(list("bc"), negate=True)
         self.assertEqual(a, {"a": "AAAAAA"})
 
-    def test_take_seqs_str(self):
+    def test_take_seqs_str(self):  # ported
         """string arg to SequenceCollection take_seqs should work."""
         a = self.ragged_padded.take_seqs("a", negate=True)
         self.assertTrue(isinstance(a, _SequenceCollectionBase))
@@ -256,7 +256,7 @@ class SequenceCollectionBaseTests(object):
         a = self.ragged_padded.take_seqs("a")
         self.assertEqual(a, {"a": "AAAAAA"})
 
-    def test_take_seqs_info(self):
+    def test_take_seqs_info(self):  # ported
         """take_seqs should preserve info attribute"""
         orig = self.Class(
             data={"a": "CCCCCC", "b": "AAA---", "c": "AAAA--"}, info={"key": "value"}
@@ -264,7 +264,7 @@ class SequenceCollectionBaseTests(object):
         subset = orig.take_seqs(list("ab"))
         self.assertEqual(set(subset.info), set(orig.info))
 
-    def test_take_seqs_moltype(self):
+    def test_take_seqs_moltype(self):  # ported
         """take_seqs should preserve the MolType"""
         orig = self.Class(
             data={"a": "CCCCCC", "b": "AAA---", "c": "AAAA--"}, moltype=DNA
@@ -449,7 +449,7 @@ class SequenceCollectionBaseTests(object):
         got = align_norm.to_nexus("protein")
         self.assertEqual(got, expect)
 
-    def test_num_seqs(self):
+    def test_num_seqs(self):  # ported
         """SequenceCollection.num_seqs should count seqs."""
         aln = self.Class({"seq1": "ACGU", "seq2": "CGUA", "seq3": "CCGU"})
         self.assertEqual(aln.num_seqs, 3)
@@ -785,7 +785,7 @@ class SequenceCollectionBaseTests(object):
                 dict(num_seqs=10, num_pos=60, ref_name="longest", wrap=60),
             )
 
-    def test_set_wrap_affects_repr_html(self):
+    def test_set_wrap_affects_repr_html(self):  # ported for seqcollection
         """the wrap argument affects the number of columns"""
         # indirectly tested via counting number of occurrences of 'class="label"'
         seqs = self.Class({"a": "AAAAA", "b": "AAA--"})
@@ -845,7 +845,7 @@ class SequenceCollectionTests(SequenceCollectionBaseTests, TestCase):
         self.ragged.names = "bac"
         self.assertEqual(list(self.ragged.seqs), ["AAA", "AAAAAA", "AAAA"])
 
-    def test_iter_seqs_ragged(self):
+    def test_iter_seqs_ragged(self):  # ported
         """SequenceCollection iter_seqs() method should support reordering of seqs"""
         self.ragged.names = ["a", "b", "c"]
         seqs = list(self.ragged.iter_seqs())
@@ -3703,7 +3703,7 @@ def test_slice_aligned_featuremap_multi_spans():
     assert str(sliced) == "AAGGGCCC"
 
 
-def test_sequence_collection_repr():
+def test_sequence_collection_repr():  # ported
     data = {
         "ENSMUSG00000056468": "GCCAGGGGGAAAAGGGAGAA",
         "ENSMUSG00000039616": "GCCCTTCAAATTT",
diff --git a/tests/test_core/test_new_alignment.py b/tests/test_core/test_new_alignment.py
index 823efa22c..0241a33c1 100644
--- a/tests/test_core/test_new_alignment.py
+++ b/tests/test_core/test_new_alignment.py
@@ -1,18 +1,13 @@
+import os
+
 import numpy as numpy
 import pytest
 
-from cogent3 import get_moltype
+import cogent3.core.new_alignment as new_aln
+import cogent3.core.new_alphabet as new_alpha
+import cogent3.core.new_moltype as new_moltype
+
 from cogent3._version import __version__
-from cogent3.core.alphabet import CharAlphabet
-from cogent3.core.new_alignment import (
-    AlignedData,
-    AlignedDataView,
-    SeqData,
-    SeqDataView,
-    seq_index,
-    seq_to_gap_coords,
-    validate_names,
-)
 
 
 @pytest.fixture
@@ -21,24 +16,25 @@ def seq1():
 
 
 @pytest.fixture
-def simple_dict():
-    return dict(seq1="ACGT", seq2="GTTTGCA")
+def str_seqs_dict(moltype="dna"):
+    if moltype == "dna":
+        return dict(seq1="ACGT", seq2="GTTTGCA")
 
 
 @pytest.fixture
-def simple_dict_arr():
+def arr_seqs_dict():
     return dict(seq1=numpy.array([2, 1, 3, 0]), seq2=numpy.array([3, 0, 0, 0, 3, 1, 2]))
 
 
 @pytest.fixture
-def alpha():
-    moltype = get_moltype("dna")
-    return moltype.alphabets.degen_gapped
+def alpha(moltype="dna"):
+    moltype = new_moltype.get_moltype(moltype)
+    return moltype.degen_gapped_alphabet
 
 
 @pytest.fixture
-def sd_demo(simple_dict: dict[str, str], alpha):
-    return SeqData(data=simple_dict, alphabet=alpha)
+def dna_sd(str_seqs_dict: dict[str, str], alpha):
+    return new_aln.SeqsData(data=str_seqs_dict, alphabet=alpha)
 
 
 @pytest.fixture
@@ -47,8 +43,14 @@ def int_arr():
 
 
 @pytest.fixture
-def sdv_s2(sd_demo: SeqData) -> SeqDataView:
-    return sd_demo.get_seq_view(seqid="seq2")
+def sdv_s2(dna_sd: new_aln.SeqsData) -> new_aln.SeqDataView:
+    return dna_sd.get_seq_view(seqid="seq2")
+
+
+@pytest.fixture
+def simple_dna_seq_coll(dna_sd: new_aln.SeqsData) -> new_aln.SequenceCollection:
+    moltype = new_moltype.get_moltype("dna")
+    return new_aln.SequenceCollection(seqs_data=dna_sd, moltype=moltype)
 
 
 @pytest.fixture
@@ -58,70 +60,45 @@ def aligned_dict():
 
 @pytest.fixture
 def ad_demo(aligned_dict: dict[str, str]):
-    return AlignedData.from_gapped_seqs(aligned_dict)
-
-
-def test_seqdata_default_attributes(sd_demo: SeqData):
-    assert sd_demo._names == ("seq1", "seq2")
-    assert isinstance(sd_demo._alphabet, CharAlphabet)
-
-
-def test_seqdata_seq_if_str(seq1: str, alpha):
-    with pytest.raises(NotImplementedError):
-        SeqData(seq1, alphabet=alpha)
-    # assert SeqData(data)._names != ("A", "C", "T", "G")
+    return new_aln.AlignedData.from_gapped_seqs(aligned_dict)
 
 
-def test_validate_names_dict_happy(simple_dict):
-    got = validate_names(simple_dict, None)
-    assert got == ("seq1", "seq2")
-    got = validate_names(simple_dict, names=("seq2", "seq1"))
-    assert got == ("seq2", "seq1")
+@pytest.fixture
+def ragged_padded():
+    return new_aln.make_unaligned_seqs(
+        data={"a": "AAAAAA", "b": "AAA---", "c": "AAAA--"}, moltype="dna"
+    )
 
 
-@pytest.mark.parametrize(
-    "bad_names", [("bad"), ("bad",), ("bad2", "bad1"), ("seq1",), "seq1"]
-)
-def test_validate_names_dict_bad(simple_dict, bad_names):
-    with pytest.raises(ValueError):
-        validate_names(simple_dict, bad_names)
-
+@pytest.fixture
+def ragged():
+    return new_aln.make_unaligned_seqs(
+        data={"a": "AAAAAA", "b": "AAA", "c": "AAAA"}, moltype="dna"
+    )
 
-@pytest.mark.parametrize(
-    "names", (["seq2"], ["seq2", "seq1"], ("seq2",), ("seq2", "seq1"))
-)
-def test_names_tuple_list_happy(names, sd_demo):
-    correct_names = sd_demo._names
-    got = validate_names(correct_names, names)
-    assert got == tuple(names)
 
+def test_seqsdata_default_attributes(dna_sd: new_aln.SeqsData):
+    assert dna_sd.names == ["seq1", "seq2"]
+    assert isinstance(dna_sd._alphabet, new_alpha.CharAlphabet)
 
-@pytest.mark.parametrize("bad_names", [("bad"), ("bad",), ("bad2", "bad1"), "seq1"])
-def test_names_tuple_list_bad(bad_names, sd_demo):
-    correct_names = sd_demo._names
-    with pytest.raises(ValueError):
-        validate_names(correct_names, bad_names)
 
+@pytest.mark.xfail(reason="SeqsData currently expects correctly formatted data")
+def test_seqsdata_seq_if_str(seq1: str, alpha):
+    with pytest.raises(NotImplementedError):
+        new_aln.SeqsData(seq1, alphabet=alpha)
 
-@pytest.mark.parametrize(
-    "bad_names", [("bad"), ("bad",), ("bad2", "bad1"), ("seq1",), "seq1"]
-)
-def test_names_init(simple_dict, alpha, bad_names):
-    sd = SeqData(simple_dict, alphabet=alpha)
-    assert sd._names == ("seq1", "seq2")
-    sd = SeqData(simple_dict, alphabet=alpha, names=("seq2", "seq1"))
-    assert sd._names == ("seq2", "seq1")
 
+def test_seqdataview_zero_step_raises(dna_sd):
     with pytest.raises(ValueError):
-        SeqData(simple_dict, alphabet=alpha, names=bad_names)
+        new_aln.SeqDataView(seq=dna_sd, step=0, seq_len=4)
 
 
 @pytest.mark.parametrize("seqid", ["seq1", "seq2"])
-def test_seqdataview_repr_default(sd_demo: SeqData, seqid: str):
-    seq = sd_demo.get_seq_str(seqid=seqid)
+def test_seqdataview_repr_default(dna_sd: new_aln.SeqsData, seqid: str):
+    seq = dna_sd.get_seq_str(seqid=seqid)
     seq_len = len(seq)
     expect = f"SeqDataView(seq={seq}, start=0, stop={seq_len}, step=1, offset=0, seqid='{seqid}', seq_len={seq_len})"
-    got = sd_demo.get_seq_view(seqid)
+    got = dna_sd.get_seq_view(seqid)
     assert repr(got) == expect
 
 
@@ -132,29 +109,34 @@ def test_seqdataview_repr_default_long(alpha):
     expect = f"SeqDataView(seq={trunc}, start=0, stop={seq_len}, step=1, offset=0, seqid='long', seq_len={seq_len})"
 
     d = {"long": longseq}
-    sd = SeqData(d, alphabet=alpha)
+    sd = new_aln.SeqsData(d, alphabet=alpha)
     got = sd.get_seq_view(seqid="long")
     assert repr(got) == expect
 
 
+@pytest.mark.xfail(reason="do we want to support copying and/or copying with slicing?")
 @pytest.mark.parametrize("seqid", ["seq1", "seq2"])
-def test_seqdataview_to_rich_dict(sd_demo: SeqData, seqid):
-    seq = sd_demo.get_seq_str(seqid=seqid)
-    expect = {
-        "type": "cogent3.core.new_alignment.SeqDataView",
-        "version": __version__,
-        "init_args": {"seq": seq, "seqid": seqid, "step": 1, "offset": 0},
-    }
+@pytest.mark.parametrize("sliced", (False, True))
+@pytest.mark.parametrize("step", (1, -1))
+def test_seqdataview_copy(alpha, seqid, sliced, step):
+    seq1 = "ATGTTCTC"
+    seq2 = "ATGAACTCATT"
+    start, stop = 2, 6
+    data = {"seq1": seq1, "seq2": seq2}
 
-    sdv = sd_demo.get_seq_view(seqid)
-    got = sdv.to_rich_dict()
-    assert expect == got
+    sd = new_aln.SeqsData(data=data, alphabet=alpha)
+    sdv = sd.get_seq_view(seqid=seqid)
+    sliced_sdv = sdv[start:stop:step]
+
+    copied_sdv = sliced_sdv.copy(sliced=sliced)
+
+    assert copied_sdv.str_value == data[seqid][start:stop:step]
 
 
 @pytest.mark.parametrize("seqid", ["seq1", "seq2"])
-def test_seqdata_get_seq_view(simple_dict, alpha, seqid):
-    sd = SeqData(simple_dict, alphabet=alpha)
-    seq = simple_dict[seqid]
+def test_seqsdata_get_seq_view(str_seqs_dict, alpha, seqid):
+    sd = new_aln.SeqsData(str_seqs_dict, alphabet=alpha)
+    seq = str_seqs_dict[seqid]
     seq_len = len(seq)
     got = sd.get_seq_view(seqid)
     assert got.seq == sd
@@ -166,124 +148,101 @@ def test_seqdata_get_seq_view(simple_dict, alpha, seqid):
 @pytest.mark.parametrize("seq", ("seq1", "seq2"))
 @pytest.mark.parametrize("start", (None, -1, 0, 1, 4))
 @pytest.mark.parametrize("stop", (None, -1, 0, 1, 4))
-def test_get_seq_str(simple_dict, alpha, seq, start, stop):
+def test_get_seq_str(str_seqs_dict, alpha, seq, start, stop):
     # slicing should be tested in test_get_seq_array
-    expect = simple_dict[seq][start:stop]
-    sd = SeqData(data=simple_dict, alphabet=alpha)
+    expect = str_seqs_dict[seq][start:stop]
+    sd = new_aln.SeqsData(data=str_seqs_dict, alphabet=alpha)
     got = sd.get_seq_str(seqid=seq, start=start, stop=stop)
     assert expect == got
 
 
-def test_get_seq_str_empty(sd_demo: SeqData):
+def test_get_seq_str_empty(dna_sd: new_aln.SeqsData):
     with pytest.raises(TypeError):
-        sd_demo.get_seq_str()
+        dna_sd.get_seq_str()
 
 
-def test_names_get(simple_dict, alpha):
-    expect = simple_dict.keys()
-    sd = SeqData(simple_dict, alphabet=alpha)
+def test_names_get(str_seqs_dict, alpha):
+    expect = str_seqs_dict.keys()
+    sd = new_aln.SeqsData(str_seqs_dict, alphabet=alpha)
     got = sd.names
     # returns iterator
     assert list(got) == list(expect)
 
 
-@pytest.mark.parametrize(
-    "seq, moltype_name",
-    [("TCAG-NRYWSKMBDHV?", "dna"), ("UCAG-NRYWSKMBDHV?", "rna")],
-)
-def test_seq_index_str(seq, moltype_name, int_arr):
-    alpha = get_moltype(moltype_name).alphabets.degen_gapped
-    got = seq_index(seq, alpha)
-    assert numpy.array_equal(got, int_arr)
-
-
-@pytest.mark.parametrize(
-    "seq, moltype_name",
-    [(b"TCAG-NRYWSKMBDHV?", "dna"), (b"UCAG-NRYWSKMBDHV?", "rna")],
-)
-def test_seq_index_bytes(seq, moltype_name, int_arr):
-    alpha = get_moltype(moltype_name).alphabets.degen_gapped
-    got = seq_index(seq, alpha)
-    assert numpy.array_equal(got, int_arr)
-
-
-@pytest.mark.parametrize("moltype_name", ("dna", "rna"))
-def test_seq_index_arr(moltype_name, int_arr):
-    alpha = get_moltype(moltype_name).alphabets.degen_gapped
-    got = seq_index(int_arr, alpha)
-    assert numpy.array_equal(got, int_arr)
-    assert got.dtype == int_arr.dtype
-
-
-def test_get_seq_array(simple_dict, alpha):
+def test_get_seq_array(str_seqs_dict, alpha):
     # TODO: slicing should be tested here, not get_seq_str
+    # todo: kath, edit and parametrize for all moltypes
     # seq1
     expect = numpy.array([2, 1, 3, 0], dtype="uint8")
-    sd = SeqData(data=simple_dict, alphabet=alpha)
+    sd = new_aln.SeqsData(data=str_seqs_dict, alphabet=alpha)
     got = sd.get_seq_array(seqid="seq1")
     assert numpy.array_equal(got, expect)
 
     # seq2
     expect = numpy.array([3, 0, 0, 0, 3, 1, 2], dtype="uint8")
-    sd = SeqData(data=simple_dict, alphabet=alpha)
+    sd = new_aln.SeqsData(data=str_seqs_dict, alphabet=alpha)
     got = sd.get_seq_array(seqid="seq2")
     assert numpy.array_equal(got, expect)
 
 
-def test_get_seq_bytes(sd_demo: SeqData):
+def test_get_seq_bytes(dna_sd: new_aln.SeqsData):
     # getting seqid and slicing tested in test_get_seq_str
-    got = sd_demo.get_seq_bytes(seqid="seq1")
+    got = dna_sd.get_seq_bytes(seqid="seq1")
     assert isinstance(got, bytes)
 
 
-# SeqData set_seq_maker tests
-def test_seqdataview_make_seq_default(sd_demo):
-    assert sd_demo.make_seq is None
+def test_seqdataview_make_seq_default(dna_sd):
+    assert dna_sd.make_seq is None
 
 
-@pytest.mark.parametrize(
-    "maker", (get_moltype("dna").make_seq, get_moltype("dna").make_array_seq)
-)
-def test_seqdataview_make_seq_setget(sd_demo, maker):
-    sd_demo.make_seq = maker
-    assert sd_demo.make_seq is maker
+def test_seqdataview_make_seq_setget(dna_sd):
+    dna_sd.make_seq = new_moltype.get_moltype("dna").make_seq
+    assert dna_sd.make_seq == new_moltype.get_moltype("dna").make_seq
 
 
-# SeqData __getitem__ tests
 @pytest.mark.parametrize("seq", ("seq1", "seq2"))
-def test_getitem_str_1(sd_demo, seq):
-    got = sd_demo[seq]
-    assert got.seq == sd_demo
+def test_getitem_str_1(dna_sd, seq):
+    got = dna_sd[seq]
+    assert got.seq == dna_sd
     assert got.seqid == seq
 
 
 @pytest.mark.xfail(reason="'SeqDataView' object has no attribute 'replace'")
 @pytest.mark.parametrize("idx", (0, 1))
 @pytest.mark.parametrize("make_seq", (True, False))
-def test_getitem_int(simple_dict, alpha, idx, make_seq):
-    sd = SeqData(simple_dict, alphabet=alpha)
-    ms = get_moltype("dna").make_seq if make_seq else None
+def test_getitem_int(str_seqs_dict, alpha, idx, make_seq):
+    sd = new_aln.SeqsData(str_seqs_dict, alphabet=alpha)
+    ms = new_moltype.get_moltype("dna").make_seq if make_seq else None
     sd.make_seq = ms
     got = sd[idx]
     assert got.seq == sd
-    assert got.seqid == list(simple_dict)[idx]
+    assert got.seqid == list(str_seqs_dict)[idx]
 
 
 @pytest.mark.xfail(reason="'SeqDataView' object has no attribute 'replace'")
 @pytest.mark.parametrize("seqid", ("seq1", "seq2"))
 @pytest.mark.parametrize("make_seq", (True, False))
-def test_getitem_str(sd_demo, seqid, make_seq):
-    ms = get_moltype("dna").make_seq if make_seq else None
-    sd_demo.make_seq = ms
-    got = sd_demo[seqid]
-    assert got.seq == sd_demo
+def test_getitem_str(dna_sd, seqid, make_seq):
+    ms = new_moltype.get_moltype("dna").make_seq if make_seq else None
+    dna_sd.make_seq = ms
+    got = dna_sd[seqid]
+    assert got.seq == dna_sd
     assert got.seqid == seqid
 
 
+@pytest.mark.parametrize("make_seq", (True, False))
+def test_getitem_raises(dna_sd, make_seq):
+    ms = new_moltype.get_moltype("dna").make_seq if make_seq else None
+    dna_sd.make_seq = ms
+    invalid_index = ["this", "shouldn't", "work"]
+    with pytest.raises(NotImplementedError):
+        _ = dna_sd[invalid_index]
+
+
 # SeqDataView tests for returning an instance of itself
-def test_seqdataview_returns_self(sd_demo: SeqData):
-    sdv = sd_demo.get_seq_view("seq1")
-    assert isinstance(sdv, SeqDataView)
+def test_seqdataview_returns_self(dna_sd: new_aln.SeqsData):
+    sdv = dna_sd.get_seq_view("seq1")
+    assert isinstance(sdv, new_aln.SeqDataView)
 
 
 @pytest.mark.parametrize(
@@ -298,19 +257,19 @@ def test_seqdataview_returns_self(sd_demo: SeqData):
     ],
 )
 def test_seqdataview_slice_returns_self(seq1: str, index: slice):
-    sdv = SeqDataView(seq=seq1, seqid="seq1", seq_len=len(seq1))
+    sdv = new_aln.SeqDataView(seq=seq1, seqid="seq1", seq_len=len(seq1))
     got = sdv[index]
-    assert isinstance(got, SeqDataView)
+    assert isinstance(got, new_aln.SeqDataView)
 
 
 # SeqDataView tests for value properties
 @pytest.mark.parametrize("start", (None, 0, 1, 4, -1, -4))
 @pytest.mark.parametrize("stop", (None, 0, 1, 4, -1, -4))
 @pytest.mark.parametrize("step", (None, 1, 2, 3, -1, -2, -3))
-def test_seqdataview_value(simple_dict: dict, alpha, start, stop, step):
+def test_seqdataview_value(str_seqs_dict: dict, alpha, start, stop, step):
     seq = "seq2"
-    expect = simple_dict[seq][start:stop:step]
-    sd = SeqData(data=simple_dict, alphabet=alpha)
+    expect = str_seqs_dict[seq][start:stop:step]
+    sd = new_aln.SeqsData(data=str_seqs_dict, alphabet=alpha)
     # Get SeqDataView on seq
     sdv = sd.get_seq_view(seqid=seq)
     sdv2 = sdv[start:stop:step]
@@ -321,10 +280,10 @@ def test_seqdataview_value(simple_dict: dict, alpha, start, stop, step):
 @pytest.mark.parametrize("start", (None, 0, 1, 4, -1, -4))
 @pytest.mark.parametrize("stop", (None, 0, 1, 4, -1, -4))
 @pytest.mark.parametrize("step", (None, 1, 2, 3, -1, -2, -3))
-def test_seqdata_array_value(simple_dict_arr: dict, alpha, start, stop, step):
+def test_seqsdata_array_value(arr_seqs_dict: dict, alpha, start, stop, step):
     seq = "seq2"
-    expect = simple_dict_arr[seq][start:stop:step]
-    sd = SeqData(data=simple_dict_arr, alphabet=alpha)
+    expect = arr_seqs_dict[seq][start:stop:step]
+    sd = new_aln.SeqsData(data=arr_seqs_dict, alphabet=alpha)
     # Get SeqDataView on seq
     sdv = sd.get_seq_view(seqid=seq)
     got = sdv.array_value[start:stop:step]
@@ -334,11 +293,11 @@ def test_seqdata_array_value(simple_dict_arr: dict, alpha, start, stop, step):
 @pytest.mark.parametrize("start", (None, 0, 1, 4, -1, -4))
 @pytest.mark.parametrize("stop", (None, 0, 1, 4, -1, -4))
 @pytest.mark.parametrize("step", (None, 1, 2, 3, -1, -2, -3))
-def test_seqdata_bytes_value(simple_dict: dict, alpha, start, stop, step):
+def test_seqsdata_bytes_value(str_seqs_dict: dict, alpha, start, stop, step):
     seq = "seq2"
-    expect = simple_dict[seq][start:stop:step]
+    expect = str_seqs_dict[seq][start:stop:step]
     expect = expect.encode("utf8")
-    sd = SeqData(data=simple_dict, alphabet=alpha)
+    sd = new_aln.SeqsData(data=str_seqs_dict, alphabet=alpha)
     # Get SeqDataView on seq
     sdv = sd.get_seq_view(seqid=seq)
     got = sdv.bytes_value[start:stop:step]
@@ -346,13 +305,13 @@ def test_seqdata_bytes_value(simple_dict: dict, alpha, start, stop, step):
 
 
 # SeqDataView tests for special methods that access "value" properties
-def test_array(sdv_s2: SeqDataView):
+def test_array(sdv_s2: new_aln.SeqDataView):
     expect = sdv_s2.array_value
     got = numpy.array(sdv_s2)
     assert numpy.array_equal(expect, got)
 
 
-def test_bytes(sdv_s2: SeqDataView):
+def test_bytes(sdv_s2: new_aln.SeqDataView):
     expect = sdv_s2.bytes_value
     got = bytes(sdv_s2)
     assert expect == got
@@ -362,53 +321,66 @@ def test_bytes(sdv_s2: SeqDataView):
 def test_from_string_unequal_seqlens():
     data = dict(seq1="A-A", seq2="AAAAAAA--")
     with pytest.raises(ValueError):
-        AlignedData.from_gapped_seqs(data=data)
+        new_aln.AlignedData.from_gapped_seqs(data=data)
 
 
+@pytest.mark.xfail(reason="AlignedData not yet functional")
 def test_aligned_from_string_returns_self(aligned_dict):
-    got = AlignedData.from_gapped_seqs(data=aligned_dict)
-    assert isinstance(got, AlignedData)
+    got = new_aln.AlignedData.from_gapped_seqs(data=aligned_dict)
+    assert isinstance(got, new_aln.AlignedData)
     # assert gap lengths
 
 
+@pytest.mark.xfail(reason="AlignedData not yet functional")
 @pytest.mark.parametrize("seqid", ("seq1", "seq2"))
 def test_get_aligned_view(aligned_dict, seqid):
-    ad = AlignedData.from_gapped_seqs(aligned_dict)
+    ad = new_aln.AlignedData.from_gapped_seqs(aligned_dict)
     got = ad.get_aligned_view(seqid)
-    assert isinstance(got, AlignedDataView)
+    assert isinstance(got, new_aln.AlignedDataView)
     assert got.seq == ad
     assert got.stop == ad.align_len
     assert got.seq_len == ad.align_len
 
 
-# AlignedData seq to gaps
+@pytest.mark.xfail(reason="AlignedData not yet functional")
 def test_seq_to_gap_coords_str_all_gaps():
     parent_seq = "-----"
     expect_gaplen = numpy.array([len(parent_seq)])
-    got_ungap, got_map = seq_to_gap_coords(parent_seq, moltype=get_moltype("dna"))
+    got_ungap, got_map = new_aln.seq_to_gap_coords(
+        parent_seq, moltype=new_moltype.get_moltype("dna")
+    )
     assert got_ungap == ""
     assert got_map.cum_gap_lengths == expect_gaplen
 
 
+@pytest.mark.xfail(reason="AlignedData not yet functional")
 def test_seq_to_gap_coords_str_no_gaps():
     parent_seq = "ACTGC"
-    got_ungap, got_empty_arr = seq_to_gap_coords(parent_seq, moltype=get_moltype("dna"))
+    got_ungap, got_empty_arr = new_aln.seq_to_gap_coords(
+        parent_seq, moltype=new_moltype.get_moltype("dna")
+    )
     assert got_ungap == parent_seq
     assert got_empty_arr.size == 0
 
 
+@pytest.mark.xfail(reason="AlignedData not yet functional")
 def test_seq_to_gap_coords_arr_all_gaps():
-    alpha = get_moltype("dna").alphabets.degen_gapped
-    parent_seq = seq_index("-----", alpha)
-    got_ungap, got_map = seq_to_gap_coords(parent_seq, moltype=get_moltype("dna"))
+    alpha = new_moltype.get_moltype("dna").degen_gapped_alphabet
+    parent_seq = alpha.to_indices("-----", alpha)
+    got_ungap, got_map = new_aln.seq_to_gap_coords(
+        parent_seq, moltype=new_moltype.get_moltype("dna")
+    )
     assert got_ungap.size == 0
     assert got_map.get_gap_coordinates() == [[0, 5]]
 
 
+@pytest.mark.xfail(reason="AlignedData not yet functional")
 def test_seq_to_gap_coords_arr_no_gaps():
-    alpha = get_moltype("dna").alphabets.degen_gapped
-    parent_seq = seq_index("ACTGC", alpha)
-    got_ungap, got_empty_arr = seq_to_gap_coords(parent_seq, moltype=get_moltype("dna"))
+    alpha = new_moltype.get_moltype("dna").degen_gapped_alphabet
+    parent_seq = alpha.to_indices("ACTGC", alpha)
+    got_ungap, got_empty_arr = new_aln.seq_to_gap_coords(
+        parent_seq, moltype=new_moltype.get_moltype("dna")
+    )
     assert numpy.array_equal(got_ungap, parent_seq)
     assert got_empty_arr.size == 0
 
@@ -422,19 +394,222 @@ def gap_seqs():
     ]
 
 
-@pytest.mark.parametrize("i", range(3))  # range(len(gap_seqs()))
+@pytest.mark.xfail(reason="AlignedData not yet functional")
+@pytest.mark.parametrize("i", range(3))
 def test_seq_to_gap_coords_str(gap_seqs, i):
     seq, gap_coords = gap_seqs[i]
-    got_ungapped, got_map = seq_to_gap_coords(seq, moltype=get_moltype("dna"))
+    got_ungapped, got_map = new_aln.seq_to_gap_coords(
+        seq, moltype=new_moltype.get_moltype("dna")
+    )
     assert got_ungapped == seq.replace("-", "")
     assert got_map.get_gap_coordinates() == gap_coords
 
 
-@pytest.mark.parametrize("i", range(3))  # range(len(gap_seqs()))
+@pytest.mark.xfail(reason="AlignedData not yet functional")
+@pytest.mark.parametrize("i", range(3))
 def test_seq_to_gap_coords_arr(gap_seqs, i):
     seq, gap_coords = gap_seqs[i]
-    alpha = get_moltype("dna").alphabets.degen_gapped
-    seq = seq_index(seq, alpha)  # convert to array repr
-    got_ungapped, got_map = seq_to_gap_coords(seq, moltype=get_moltype("dna"))
+    alpha = new_moltype.get_moltype("dna").degen_gapped_alphabet
+    seq = alpha.to_indices(seq, alpha)  # convert to array repr
+    got_ungapped, got_map = new_aln.seq_to_gap_coords(
+        seq, moltype=new_moltype.get_moltype("dna")
+    )
     assert numpy.array_equal(got_ungapped, seq[seq != 4])  # gap_char = 4
     assert got_map.get_gap_coordinates() == gap_coords
+
+
+def test_seq_coll_init(simple_dna_seq_coll):
+    assert isinstance(simple_dna_seq_coll.seqs, new_aln.SeqsData)
+    assert simple_dna_seq_coll.seqs.make_seq is not None
+
+
+@pytest.mark.parametrize("moltype", ("dna", "rna", "protein", "protein_with_stop"))
+def test_make_unaligned_seqs_dict(moltype):
+    """test SequenceCollection constructor utility function"""
+    data = {"a": "AGGCCC", "b": "AGAAAA"}
+    got = new_aln.make_unaligned_seqs(data=data, moltype=moltype)
+    assert isinstance(got, new_aln.SequenceCollection)
+    assert got._seqs_data.make_seq == new_moltype.get_moltype(moltype).make_seq
+
+    # should also work if seqs are arrays
+    data = {"a": numpy.array(["AGGCCC"]), "b": numpy.array(["AGAAAA"])}
+    got = new_aln.make_unaligned_seqs(data=data, moltype=moltype)
+    assert isinstance(got, new_aln.SequenceCollection)
+    assert got._seqs_data.make_seq == new_moltype.get_moltype(moltype).make_seq
+
+
+@pytest.mark.parametrize("moltype", ("dna", "rna", "protein", "protein_with_stop"))
+def test_make_unaligned_seqs_list(moltype):
+    """test SequenceCollection constructor utility function"""
+    data = ["AGGCCC", "AGAAAA"]
+    got = new_aln.make_unaligned_seqs(data=data, moltype=moltype)
+    assert isinstance(got, new_aln.SequenceCollection)
+    assert got._seqs_data.make_seq == new_moltype.get_moltype(moltype).make_seq
+
+    # should also work if seqs are arrays
+    data = [numpy.array(["AGGCCC"]), numpy.array(["AGAAAA"])]
+    got = new_aln.make_unaligned_seqs(data=data, moltype=moltype)
+    assert isinstance(got, new_aln.SequenceCollection)
+    assert got._seqs_data.make_seq == new_moltype.get_moltype(moltype).make_seq
+
+
+def test_make_unaligned_seqs_raises():
+    data = "AGTCCTGA"
+    with pytest.raises(NotImplementedError):
+        new_aln.make_unaligned_seqs(data=data, moltype="dna")
+
+
+def test_make_unaligned_seqs_incompatible_moltype(dna_sd):
+    with pytest.raises(ValueError):
+        _ = new_aln.make_unaligned_seqs(data=dna_sd, moltype="rna")
+
+
+def test_make_unaligned_seqs_no_seqs():
+    data = {}
+    with pytest.raises(ValueError):
+        new_aln.make_unaligned_seqs(data=data, moltype="dna")
+
+
+@pytest.mark.xfail(
+    reason="todo: kath, AttributeError: 'SeqDataView' object has no attribute 'replace'"
+)
+def test_iter_seqs_ragged_padded(ragged_padded):
+    """SequenceCollection.iter_seqs() method should support reordering of seqs"""
+    seqs = list(ragged_padded.iter_seqs())
+    assert seqs == ["AAAAAA", "AAA---", "AAAA--"]
+    seqs = list(ragged_padded.iter_seqs(seq_order=["b", "a", "a"]))
+    assert seqs == ["AAA---", "AAAAAA", "AAAAAA"]
+    assert seqs[1] is seqs[2]
+    assert seqs[0], ragged_padded.seqs["b"]
+
+
+@pytest.mark.xfail(
+    reason="todo: kath, AttributeError: 'SeqDataView' object has no attribute 'replace'"
+)
+def test_iter_seqs_ragged(ragged):
+    """SequenceCollection iter_seqs() method should support reordering of seqs"""
+    seqs = list(ragged.iter_seqs())
+    assert seqs == ["AAAAAA", "AAA", "AAAA"]
+    seqs = list(ragged.iter_seqs(seq_order=["b", "a", "a"]))
+    assert seqs == ["AAA", "AAAAAA", "AAAAAA"]
+    assert seqs[1] is seqs[2]
+    assert seqs[0], ragged.seqs["b"]
+
+
+def test_sequence_collection_repr():
+    data = {
+        "ENSMUSG00000056468": "GCCAGGGGGAAAAGGGAGAA",
+        "ENSMUSG00000039616": "GCCCTTCAAATTT",
+    }
+    seqs = new_aln.make_unaligned_seqs(data=data, moltype="dna")
+    assert (
+        repr(seqs)
+        == "2x (ENSMUSG00000039616[GCCCTTCAAA...], ENSMUSG00000056468[GCCAGGGGGA...]) dna seqcollection"
+    )
+
+    data = {
+        "ENSMUSG00000039616": "GCCCTTCAAATTT",
+        "ENSMUSG00000056468": "GCCAGGGGGAAAAGGGAGAA",
+    }
+    seqs = new_aln.make_unaligned_seqs(data=data, moltype="dna")
+    assert (
+        repr(seqs)
+        == "2x (ENSMUSG00000039616[GCCCTTCAAA...], ENSMUSG00000056468[GCCAGGGGGA...]) dna seqcollection"
+    )
+
+    data = {
+        "a": "TCGAT",
+    }
+    seqs = new_aln.make_unaligned_seqs(data=data, moltype="dna")
+    assert repr(seqs) == "1x (a[TCGAT]) dna seqcollection"
+
+    data = {
+        "a": "TCGAT" * 2,
+    }
+    seqs = new_aln.make_unaligned_seqs(data=data, moltype="dna")
+    assert repr(seqs) == "1x (a[TCGATTCGAT]) dna seqcollection"
+
+
+@pytest.mark.xfail(
+    reason="todo: kath, new 'MolType' object has no attribute 'get_css_style'"
+)
+def test_set_wrap_affects_repr_html():
+    """the wrap argument affects the number of columns"""
+    # indirectly tested via counting number of occurrences of 'class="label"'
+    seqs = new_aln.make_unaligned_seqs(data={"a": "AAAAA", "b": "AAA--"}, moltype="dna")
+    orig = seqs._repr_html_()
+    seqs.set_repr_policy(wrap=3)  # break alignment into 2
+    got = seqs._repr_html_()
+    token = 'class="label"'
+    assert got.count(token) == 2 * orig.count(token)
+
+    # using environment variable
+    env_name = "COGENT3_ALIGNMENT_REPR_POLICY"
+    os.environ[env_name] = "wrap=2"
+    seqs = new_aln.make_unaligned_seqs(data={"a": "AAAAA", "b": "AAA--"}, moltype="dna")
+    got = seqs._repr_html_()
+    os.environ.pop(env_name, None)
+    assert got.count(token) == 3 * orig.count(token)
+
+
+@pytest.mark.xfail(reason="todo: kath, need support for __eq__ betwen collections")
+def test_take_seqs(ragged_padded):
+    """SequenceCollection take_seqs should return new SequenceCollection with selected seqs."""
+    a = ragged_padded.take_seqs(list("bc"))
+    assert isinstance(a, new_aln.SequenceCollection)
+    assert a == {"b": "AAA---", "c": "AAAA--"}
+    # should be able to negate
+    a = ragged_padded.take_seqs(list("bc"), negate=True)
+    assert a == {"a": "AAAAAA"}
+
+
+@pytest.mark.xfail(reason="todo: kath, need support for __eq__ betwen collections")
+def test_take_seqs_str(ragged_padded):
+    """string arg to SequenceCollection take_seqs should work."""
+    a = ragged_padded.take_seqs("a")
+    assert a == {"a": "AAAAAA"}
+
+    # should be able to negate
+    a = ragged_padded.take_seqs("a", negate=True)
+    assert isinstance(a, new_aln.SequenceCollection)
+    assert a == {"b": "AAA---", "c": "AAAA--"}
+
+
+def test_take_seqs_info():
+    """take_seqs should preserve info attribute"""
+    moltype = new_moltype.get_moltype("dna")
+    orig = new_aln.make_unaligned_seqs(
+        data={"a": "CCCCCC", "b": "AAA---", "c": "AAAA--"},
+        moltype=moltype,
+        info={"key": "value"},
+    )
+    subset = orig.take_seqs(list("ab"))
+    assert set(subset.info) == set(orig.info)
+
+
+def test_take_seqs_moltype():
+    """take_seqs should preserve the MolType"""
+    moltype = new_moltype.get_moltype("dna")
+    orig = new_aln.make_unaligned_seqs(
+        data={"a": "CCCCCC", "b": "AAA---", "c": "AAAA--"}, moltype=moltype
+    )
+    subset = orig.take_seqs(list("ab"))
+    assert set(subset.moltype) == set(orig.moltype)
+
+
+def test_take_seqs_empty_names():
+    moltype = new_moltype.get_moltype("dna")
+    orig = new_aln.make_unaligned_seqs(
+        data={"a": "CCCCCC", "b": "AAA---", "c": "AAAA--"}, moltype=moltype
+    )
+    subset = orig.take_seqs([])
+    assert subset == {}
+
+
+def test_num_seqs():
+    """SequenceCollection.num_seqs should count seqs."""
+
+    aln = new_aln.make_unaligned_seqs(
+        data={"seq1": "ACGU", "seq2": "CGUA", "seq3": "CCGU"}, moltype="rna"
+    )
+    assert aln.num_seqs == 3