Repository: cogent3/cogent3
Pull request #1911 (2024-06-24 03:41:57)

Merged changes from develop


Patch:
diff --git a/pyproject.toml b/pyproject.toml
index 8aac8d9f8..4637e9d0e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -15,7 +15,7 @@ readme = "README.md"
 license = { file = "LICENSE" }
 dependencies = ["chardet",
         # until we know all dependencies work on numpy 2, pin to lesser version
-        "numpy<2",
+        "numpy",
         "numba>0.53; python_version>='3.9' and python_version <'3.12'",
         "numba>0.54; python_version>='3.10' and python_version <'3.12'",
         "numba>=0.57.0; python_version=='3.11'",
diff --git a/requirements.txt b/requirements.txt
index 5d188687d..2b6fca445 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,7 +1,7 @@
 chardet==5.2.0
 pillow==10.3.0
 psutil==5.9.8
-numpy==1.26.4
+numpy==2.0.0
 numba>0.48.0;python_version<'3.9'
 numba>0.53; python_version>='3.9'
 numba>0.54; python_version>='3.10'
diff --git a/src/cogent3/app/data_store.py b/src/cogent3/app/data_store.py
index 9b2e1c02e..2854cdf82 100644
--- a/src/cogent3/app/data_store.py
+++ b/src/cogent3/app/data_store.py
@@ -488,6 +488,7 @@ class DataStoreDirectory(DataStoreABC):
         sfx, cmp = get_format_suffixes(unique_id)
         if sfx != suffix:
             unique_id = f"{Path(unique_id).stem}.{suffix}"
+            sfx, cmp = get_format_suffixes(unique_id)
 
         unique_id = (
             unique_id.replace(self.suffix, suffix)
@@ -496,8 +497,9 @@ class DataStoreDirectory(DataStoreABC):
         )
         if suffix != "log" and unique_id in self:
             return None
-
-        with open_(self.source / subdir / unique_id, mode="w", newline="\n") as out:
+        newline = None if cmp else "\n"
+        mode = "wt" if cmp else "w"
+        with open_(self.source / subdir / unique_id, mode=mode, newline=newline) as out:
             out.write(data)
 
         if subdir == _LOG_TABLE:
diff --git a/src/cogent3/core/alignment.py b/src/cogent3/core/alignment.py
index 5bc4d6605..8f5f61eea 100644
--- a/src/cogent3/core/alignment.py
+++ b/src/cogent3/core/alignment.py
@@ -81,7 +81,7 @@ from cogent3.core.profile import PSSM, MotifCountsArray
 from cogent3.core.sequence import ArraySequence, Sequence, frac_same
 # which is a circular import otherwise.
 from cogent3.format.alignment import save_to_filename
-from cogent3.format.fasta import alignment_to_fasta
+from cogent3.format.fasta import seqs_to_fasta
 from cogent3.format.nexus import nexus_from_alignment
 from cogent3.format.phylip import alignment_to_phylip
 from cogent3.maths.stats.number import CategoryCounter
@@ -898,7 +898,7 @@ class _SequenceCollectionBase:
         -------
         The Fasta formatted alignment.
         """
-        return alignment_to_fasta(self.to_dict(), block_size=block_size)
+        return seqs_to_fasta(self.to_dict(), block_size=block_size)
 
     def to_nexus(self, seq_type, wrap=50):  # will not port
         """
diff --git a/src/cogent3/core/location.py b/src/cogent3/core/location.py
index e2a14e282..b254a43b1 100644
--- a/src/cogent3/core/location.py
+++ b/src/cogent3/core/location.py
@@ -916,7 +916,7 @@ def _spans_from_locations(locations: SeqCoordTypes, parent_length: int) -> SeqSp
         return ()
 
     if locations[0][0] > locations[-1][1]:
-        raise ValueError("locations must be ordered smallest-> largest")
+        raise ValueError(f"locations must be ordered smallest-> largest {locations}")
 
     spans = []
     for start, end in locations:
diff --git a/src/cogent3/core/new_alphabet.py b/src/cogent3/core/new_alphabet.py
index 27cac65c6..7ae03e548 100644
--- a/src/cogent3/core/new_alphabet.py
+++ b/src/cogent3/core/new_alphabet.py
@@ -462,6 +462,11 @@ def seq_to_kmer_indices(
         raise ValueError(f"size of result {len(result)} <= {size}")
 
     missing_index: int = gap_index + 1 if gap_char_index > 0 else num_states**k
+    if gap_char_index > 0:
+        assert (
+            gap_index > 0
+        ), f"gap_index={gap_index} but gap_char_index={gap_char_index}"
+
     for result_idx, i in enumerate(range(0, len(seq) - k + 1, step)):
         segment = seq[i : i + k]
         if (num_states > segment).all():
diff --git a/src/cogent3/core/sequence.py b/src/cogent3/core/sequence.py
index caecafb92..18b550389 100644
--- a/src/cogent3/core/sequence.py
+++ b/src/cogent3/core/sequence.py
@@ -57,7 +57,7 @@ from cogent3.core.annotation_db import (
 from cogent3.core.genetic_code import get_code
 from cogent3.core.info import Info as InfoClass
 from cogent3.core.location import FeatureMap, IndelMap, LostSpan
-from cogent3.format.fasta import alignment_to_fasta
+from cogent3.format.fasta import seqs_to_fasta
 from cogent3.maths.stats.contingency import CategoryCounts
 from cogent3.maths.stats.number import CategoryCounter
 from cogent3.util import warning as c3warn
@@ -124,7 +124,7 @@ class SequenceI(object):
             label = self.label
         elif hasattr(self, "name") and self.name:
             label = self.name
-        return alignment_to_fasta({label: str(self)}, block_size=block_size)
+        return seqs_to_fasta({label: str(self)}, block_size=block_size)
 
     def to_rich_dict(self, exclude_annotations=False):
         """returns {'name': name, 'seq': sequence, 'moltype': moltype.label}"""
@@ -2582,7 +2582,7 @@ class ArraySequenceBase(object):
         elif hasattr(self, "name") and self.name:
             label = self.name
 
-        return alignment_to_fasta({label: str(self)}, block_size=block_size)
+        return seqs_to_fasta({label: str(self)}, block_size=block_size)
 
     def to_phylip(self, name_len=28, label_len=30):
         """Return string of self in one line for PHYLIP, no newline.
diff --git a/src/cogent3/format/alignment.py b/src/cogent3/format/alignment.py
index 56c285036..293744829 100644
--- a/src/cogent3/format/alignment.py
+++ b/src/cogent3/format/alignment.py
@@ -1,8 +1,7 @@
-#!/usr/bin/env python
 import os
 import re
 
-from cogent3.format.fasta import alignment_to_fasta
+from cogent3.format.fasta import seqs_to_fasta
 from cogent3.format.gde import alignment_to_gde
 from cogent3.format.paml import alignment_to_paml
 from cogent3.format.phylip import alignment_to_phylip
@@ -49,8 +48,8 @@ def write_alignment_to_file(f, alignment, format, **kw):
 FORMATTERS = {
     "phylip": alignment_to_phylip,
     "paml": alignment_to_paml,
-    "fasta": alignment_to_fasta,
-    "mfa": alignment_to_fasta,
-    "fa": alignment_to_fasta,
+    "fasta": seqs_to_fasta,
+    "mfa": seqs_to_fasta,
+    "fa": seqs_to_fasta,
     "gde": alignment_to_gde,
 }
diff --git a/src/cogent3/format/fasta.py b/src/cogent3/format/fasta.py
index 28a3bf375..07850c373 100644
--- a/src/cogent3/format/fasta.py
+++ b/src/cogent3/format/fasta.py
@@ -1,40 +1,64 @@
-#!/usr/bin/env python
 """Writer for FASTA sequence format
 """
 
+import textwrap
+
 from typing import Optional
 
 from cogent3.format.util import _AlignmentFormatter
+from cogent3.util import warning as c3warn
 
 
-def alignment_to_fasta(
-    alignment_dict: dict[str, str],
+@c3warn.deprecated_args(
+    "2024.9", reason="better name", old_new=[("alignment_dict", "seqs")]
+)
+def seqs_to_fasta(
+    seqs: dict[str, str],
     block_size: int = 60,
     order: Optional[list[str]] = None,
 ) -> str:
     """Returns a Fasta string given an alignment.
 
     Parameters
-        ----------
-        alignment_dict
-            dict of seq_name
-        block_size
-            the sequence length to write to each line,
-            by default 60
-        order
-            optional list of sequence names, which order to print in.
-            Assumes complete and correct list of names,
-            by default None
+    ----------
+    seqs
+        seq_name tp sequence mapping
+    block_size
+        the sequence length to write to each line,
+        by default 60
+    order
+        optional list of sequence names, which order to print in.
+        Assumes complete and correct list of names. Defaults to
+        iteration order of seqs.
 
-        Returns
-        -------
-        The alignment in the Fasta format.
+    Returns
+    -------
+    The alignment in the Fasta format.
     """
-    order = order or []
-    return FastaFormatter().format(alignment_dict, block_size, order)
+    order = order or seqs
+    result = []
+    for name in order:
+        result.append(f">{name}")
+        result.extend(textwrap.wrap(str(seqs[name]), block_size))
+    if result:
+        result.append("")
+    return "\n".join(result)
+
+
+@c3warn.deprecated_callable("2024.9", reason="better name", new="seqs_to_fasta")
+def alignment_to_fasta(
+    alignment_dict: dict[str, str],
+    block_size: int = 60,
+    order: Optional[list[str]] = None,
+) -> str:  # pragma: no cover
+    """use seqs_to_fasta() instead"""
+    return seqs_to_fasta(seqs=alignment_dict, block_size=block_size, order=order)
 
 
-class FastaFormatter(_AlignmentFormatter):
+@c3warn.deprecated_callable(
+    "2024.9", reason="inefficient", is_discontinued=True, new="seqs_to_fasta"
+)
+class FastaFormatter(_AlignmentFormatter):  # pragma: no cover
     def format(
         self,
         alignment_dict: dict[str, str],
diff --git a/src/cogent3/maths/markov.py b/src/cogent3/maths/markov.py
index 8dbe448e7..d836c32c5 100644
--- a/src/cogent3/maths/markov.py
+++ b/src/cogent3/maths/markov.py
@@ -31,8 +31,8 @@ class TransitionMatrix(object):
     def _getStationaryProbs(self):
         if self._stationary_probs is None:
             matrix = self.Matrix
-            for i in range(10):
-                matrix = numpy.core.multiarray.dot(matrix, matrix)
+            for _ in range(10):
+                matrix = numpy.dot(matrix, matrix)
             self._stationary_probs = matrix[0]
         return self._stationary_probs
 
diff --git a/src/cogent3/util/dict_array.py b/src/cogent3/util/dict_array.py
index 4d1d47d52..96809f0c0 100644
--- a/src/cogent3/util/dict_array.py
+++ b/src/cogent3/util/dict_array.py
@@ -392,7 +392,7 @@ class DictArray(object):
 
         return self.template.wrap(self.array + other.array)
 
-    def __array__(self, dtype=None):
+    def __array__(self, dtype=None, copy=False):
         array = self.array
         if dtype is not None:
             array = array.astype(dtype)


Test patch:
diff --git a/tests/test_app/test_data_store.py b/tests/test_app/test_data_store.py
index fac1d8d1c..9f0e03c3c 100644
--- a/tests/test_app/test_data_store.py
+++ b/tests/test_app/test_data_store.py
@@ -731,3 +731,15 @@ def test_write_multiple_times_apply_to(app_dstore_in):
     orig_length = len(app.data_store)
     app.apply_to(dstore_in)
     assert len(app.data_store) == orig_length
+
+
+def test_directory_data_store_write_compressed(tmp_path):
+    from cogent3 import get_app, make_aligned_seqs, open_data_store
+
+    out = open_data_store(base_path=tmp_path / "demo", suffix="fa.gz", mode="w")
+    writer = get_app("write_seqs", data_store=out)
+    seqs = make_aligned_seqs(
+        dict(s1="CG--T", s2="CGTTT"), moltype="dna", info=dict(source="test")
+    )
+    got = writer(seqs)  # pylint: disable=not-callable
+    assert got, got
diff --git a/tests/test_core/test_new_alphabet.py b/tests/test_core/test_new_alphabet.py
index 76dd3f275..0b46c89ab 100644
--- a/tests/test_core/test_new_alphabet.py
+++ b/tests/test_core/test_new_alphabet.py
@@ -204,7 +204,8 @@ def test_seq_to_kmer_indices_handle_gap(indices, gap_index):
     coeffs = new_alphabet.coord_conversion_coeffs(4, k)
     seq = numpy.array([0, 1, 1, 3], dtype=numpy.uint8)
     result = numpy.zeros(len(seq) // k, dtype=numpy.uint8)
-    seq.put(indices, gap_index)
+    # the gap index is 4
+    seq.put(indices, 4)
     got = new_alphabet.seq_to_kmer_indices(
         seq,
         result,
diff --git a/tests/test_format/test_fasta.py b/tests/test_format/test_fasta.py
index d3b83dec1..644bdd676 100644
--- a/tests/test_format/test_fasta.py
+++ b/tests/test_format/test_fasta.py
@@ -6,7 +6,7 @@ from unittest import TestCase
 from cogent3.core.alignment import Alignment
 from cogent3.core.info import Info
 from cogent3.core.sequence import Sequence
-from cogent3.format.fasta import alignment_to_fasta
+from cogent3.format.fasta import seqs_to_fasta
 
 
 class FastaTests(TestCase):
@@ -45,9 +45,9 @@ class FastaTests(TestCase):
 
     def test_alignment_to_fasta(self):
         """should return correct fasta string."""
-        self.assertEqual(alignment_to_fasta({}), "")
-        self.assertEqual(alignment_to_fasta(self.alignment_dict), self.fasta_with_label)
+        self.assertEqual(seqs_to_fasta({}), "")
+        self.assertEqual(seqs_to_fasta(self.alignment_dict), self.fasta_with_label)
         self.assertEqual(
-            alignment_to_fasta(self.alignment_dict, block_size=2),
+            seqs_to_fasta(self.alignment_dict, block_size=2),
             self.fasta_with_label_lw2,
         )
diff --git a/tests/test_util/test_table.py b/tests/test_util/test_table.py
index 2e5067b2a..756f388d4 100644
--- a/tests/test_util/test_table.py
+++ b/tests/test_util/test_table.py
@@ -14,6 +14,7 @@ import numpy
 import pytest
 
 from numpy import arange
+from numpy.exceptions import ComplexWarning
 from numpy.testing import assert_equal
 
 from cogent3 import load_table, make_table, open_
@@ -1766,7 +1767,7 @@ class TableTests(TestCase):
 
         with numpy.testing.suppress_warnings() as sup:
             # we know that converting to real loses imaginary
-            sup.filter(numpy.ComplexWarning)
+            sup.filter(ComplexWarning)
             for d_type in [numpy.int64, numpy.complex128, numpy.float64]:
                 d = d.astype(d_type)
                 r = cast_str_to_numeric(d)