Repository: cogent3/cogent3
Pull request #2317 (2025-05-18 23:06:04)

Improve consistency in treating strand information with an Enum
## Summary by Sourcery

Introduce a Strand enum for representing and normalizing strand information, and refactor annotation and location modules, database schemas, and tests to use it consistently.

Enhancements:
- Define Strand enum in core.location with from_value, __int__, and __str__ methods for robust strand handling
- Change strand fields and parameters across annotation_db, annotation, new_alignment, new_sequence, and location modules to use the new Enum and store integer values
- Update database schemas (SQLite, GFF, Genbank) to store strand as INTEGER and convert values via Strand.from_value
- Improve Feature class to hold strand internally as an Enum, adjust equality, to_dict, and reversed logic accordingly

Documentation:
- Update changelog to document the new Strand enum and its use for consistent strand handling

Tests:
- Add parameterized tests for Strand.from_value and adapt existing annotation tests to expect numeric strand values


Patch:
diff --git a/changelog.d/20250515_083521_Gavin.Huttley.md b/changelog.d/20250515_083521_Gavin.Huttley.md
index 600f65aed..5ab365d85 100644
--- a/changelog.d/20250515_083521_Gavin.Huttley.md
+++ b/changelog.d/20250515_083521_Gavin.Huttley.md
@@ -16,7 +16,8 @@ Uncomment the section that is right (remove the HTML comment wrapper).
 - Dotplots now show the alignment path whenever Alignment.dotplot() is invoked.
   Previously, the path was only displayed if there gaps, which could be confusing
   since that's behaviour of SequenceCollection.dotplot().
-
+- Added Enum for Strand information. Developers writing their own annotation db
+  classes should employ this Enum to ensure that the strand information is consistent.
 
 ### Bug fixes
 
diff --git a/src/cogent3/core/annotation.py b/src/cogent3/core/annotation.py
index e5da65503..4f8f42962 100644
--- a/src/cogent3/core/annotation.py
+++ b/src/cogent3/core/annotation.py
@@ -4,7 +4,7 @@ from collections.abc import Iterable
 import typing_extensions
 from numpy import array
 
-from .location import FeatureMap
+from .location import FeatureMap, SeqCoordTypes, Strand
 
 if typing.TYPE_CHECKING:
     from cogent3.core.new_alignment import Alignment
@@ -40,7 +40,7 @@ class Feature:
         map: FeatureMap,
         biotype: str,
         name: str,
-        strand: str,
+        strand: int | str,
         xattr: dict[str, typing.Any] | None = None,
     ) -> None:
         # _serialisable is used for creating derivative instances
@@ -55,11 +55,11 @@ class Feature:
         data = [id(self.parent), tuple(self.map.get_coordinates())]
         data.extend((self.seqid, self.biotype, self.name))
         self._id = hash(tuple(data))
-        self._strand = strand
+        self._strand = Strand.from_value(strand)
         self._xattr = xattr
 
-    def __eq__(self, other: typing_extensions.Self) -> bool:
-        return self._id == other._id
+    def __eq__(self, other: object) -> bool:
+        return self._id == getattr(other, "_id", None)
 
     def __hash__(self) -> int:
         """Features can be used in a dictionary!"""
@@ -172,7 +172,7 @@ class Feature:
         )
         return f"{name}({txt})"
 
-    def remapped_to(self, grandparent, gmap):
+    def remapped_to(self, grandparent, gmap) -> typing_extensions.Self:
         # grandparent can be either a Sequence or an Alignment
         if not isinstance(gmap, FeatureMap):
             # due to separation of IndelMap and Map, change class
@@ -187,7 +187,7 @@ class Feature:
         }
         return self.__class__(**kwargs)
 
-    def get_coordinates(self) -> list[tuple[int, int]]:
+    def get_coordinates(self) -> SeqCoordTypes:
         """returns sequence coordinates of this Feature as
         [(start1, end1), ...]"""
         return self.map.get_coordinates()
@@ -280,7 +280,7 @@ class Feature:
         # the covered method drops reversed status so we need to
         # resurrect that, but noting we've not checked consistency
         # across the features
-        strand = self._strand if same_orientation else "+"
+        strand = self._strand.value if same_orientation else Strand.PLUS.value
         seqid = ", ".join(seqids) if seqids else None
         biotype = ", ".join(biotypes)
         kwargs = {
@@ -313,7 +313,7 @@ class Feature:
     @property
     def reversed(self) -> bool:
         """whether Feature is on the reverse strand relative to bound object"""
-        return self._strand == "-"
+        return self._strand is Strand.MINUS
 
     @property
     def xattr(self) -> dict[str, typing.Any] | None:
diff --git a/src/cogent3/core/annotation_db.py b/src/cogent3/core/annotation_db.py
index 7c5299f7a..423096cfb 100644
--- a/src/cogent3/core/annotation_db.py
+++ b/src/cogent3/core/annotation_db.py
@@ -16,6 +16,7 @@ import numpy
 import typing_extensions
 
 from cogent3._version import __version__
+from cogent3.core.location import Strand
 from cogent3.core.table import Table
 from cogent3.parse.gff import merged_gff_records
 from cogent3.util.deserialise import register_deserialiser
@@ -90,7 +91,7 @@ class FeatureDataType(typing.TypedDict):
     biotype: str  # rename type attr of cogent3 Annotatables to match this?
     name: str  # rename to name to match cogent3 Annotatable.name?
     spans: list[tuple[int, int]] | numpy.ndarray
-    strand: str  # "-" if feature on reverse strand
+    strand: int | str  # will be transformed by Strand Enum
     on_alignment: bool  # True if feature on an alignment
     xattr: dict[str, typing.Any]  # extra attributes
 
@@ -631,7 +632,7 @@ class SqliteAnnotationDbMixin:
         "parent_id": "TEXT",
         "start": "INTEGER",
         "stop": "INTEGER",
-        "strand": "TEXT",
+        "strand": "INTEGER",
         "spans": "array",
         "attributes": "TEXT",
         "on_alignment": "INT",
@@ -776,9 +777,9 @@ class SqliteAnnotationDbMixin:
         seqid: str,
         biotype: str,
         name: str,
-        spans: list[tuple[int, int]],
+        spans: list[tuple[int, int]] | numpy.ndarray,
         parent_id: OptionalStr = None,
-        strand: OptionalStr = None,
+        strand: str | int | None = None,
         attributes: OptionalStr = None,
         on_alignment: OptionalBool = False,
     ) -> None:
@@ -808,7 +809,10 @@ class SqliteAnnotationDbMixin:
         stop = int(spans.max())
         # we define record as all defined variables from local name space,
         # excluding "self"
-        record = {k: v for k, v in locals().items() if k != "self" and v is not None}
+        local_vars = locals()
+        record = {k: v for k, v in local_vars.items() if k != "self" and v is not None}
+        if "strand" in record:
+            record["strand"] = Strand.from_value(strand).value
         sql, values = _add_record_sql("user", record)
         self._execute_sql(sql, values=values)
 
@@ -931,7 +935,11 @@ class SqliteAnnotationDbMixin:
         # a record is Everything, a Feature is a subset
         # we define query as all defined variables from local name space,
         # excluding "self" and kwargs at default values
-        kwargs = {k: v for k, v in locals().items() if k != "self" and v is not None}
+        local_vars = locals()
+        kwargs = {k: v for k, v in local_vars.items() if k != "self" and v is not None}
+        if "strand" in kwargs:
+            kwargs["strand"] = Strand.from_value(strand).value
+
         # alignment features are created by the user specific
         table_names = ["user"] if on_alignment else self.table_names
         for table_name in table_names:
@@ -954,7 +962,11 @@ class SqliteAnnotationDbMixin:
         # returns essential values to create a Feature
         # we define query as all defined variables from local name space,
         # excluding "self" and kwargs with a default value of None
-        kwargs = {k: v for k, v in locals().items() if k != "self" and v is not None}
+        local_vars = locals()
+        kwargs = {k: v for k, v in local_vars.items() if k != "self" and v is not None}
+        if "strand" in kwargs:
+            kwargs["strand"] = Strand.from_value(strand).value
+
         # alignment features are created by the user specific
         table_names = ["user"] if on_alignment else self.table_names
         for table_name in table_names:
@@ -987,7 +999,10 @@ class SqliteAnnotationDbMixin:
         on_alignment: OptionalBool = None,
     ) -> int:
         """return the number of records matching condition"""
-        kwargs = {k: v for k, v in locals().items() if k != "self"}
+        local_vars = locals()
+        kwargs = {k: v for k, v in local_vars.items() if k != "self" and v is not None}
+        if "strand" in kwargs:
+            kwargs["strand"] = Strand.from_value(kwargs["strand"]).value
         num = 0
         for table_name in self.table_names:
             sql, values = _count_records_sql(table_name, conditions=kwargs)
@@ -1265,8 +1280,15 @@ class SqliteAnnotationDbMixin:
         # make sure python, not numpy, integers
         start = start if start is None else int(start)
         stop = stop if stop is None else int(stop)
+        local_vars = locals()
 
-        kwargs = {k: v for k, v in locals().items() if k not in {"self", "source"}}
+        kwargs = {
+            k: v
+            for k, v in local_vars.items()
+            if k not in {"self", "source"} and v is not None
+        }
+        if "strand" in kwargs:
+            kwargs["strand"] = Strand.from_value(strand).value
 
         result = self.__class__(source=source)
         if not len(self):
@@ -1386,7 +1408,7 @@ class GffAnnotationDb(SqliteAnnotationDbMixin, AnnotationDbABC):
         "start": "INTEGER",
         "stop": "INTEGER",
         "score": "TEXT",  # check defn
-        "strand": "TEXT",
+        "strand": "INTEGER",
         "phase": "TEXT",
         "attributes": "TEXT",
         "comments": "TEXT",
@@ -1434,6 +1456,7 @@ class GffAnnotationDb(SqliteAnnotationDbMixin, AnnotationDbABC):
             record["start"] = int(spans.min())
             record["stop"] = int(spans.max())
             record["spans"] = spans
+            record["strand"] = Strand.from_value(record.get("strand")).value
             rows.append(tuple(record.get(c) for c in col_order))
 
         self.db.executemany(sql, rows)
@@ -1493,7 +1516,7 @@ class GenbankAnnotationDb(SqliteAnnotationDbMixin, AnnotationDbABC):
         "biotype": "TEXT",  # type in GFF
         "start": "INTEGER",
         "stop": "INTEGER",
-        "strand": "TEXT",
+        "strand": "INTEGER",
         "comments": "TEXT",
         "spans": "array",  # aggregation of coords across records
         "name": "TEXT",
@@ -1549,7 +1572,7 @@ class GenbankAnnotationDb(SqliteAnnotationDbMixin, AnnotationDbABC):
             if location := record.get("location", None):
                 store["spans"] = numpy.array(location.get_coordinates(), dtype=int)
                 if strand := location.strand:
-                    store["strand"] = "-" if strand == -1 else "+"
+                    store["strand"] = Strand.from_value(strand).value
                 store["start"] = int(store["spans"].min())
                 store["stop"] = int(store["spans"].max())
 
@@ -1709,13 +1732,16 @@ def convert_annotation_to_annotation_db(data: dict) -> SupportsFeatures:
 
     db = BasicAnnotationDb()
 
+    minus = Strand.MINUS
+    plus = Strand.PLUS
+
     seqid = data.pop("name", data.pop("seqid", None))
     anns = data.pop("data")
     for ann in anns:
         ann = ann.pop("annotation_construction")  # noqa: PLW2901
         m = deserialise_map_spans(ann.pop("map"))
         spans = m.get_coordinates()
-        strand = "-" if any(s.reverse for s in m.spans) else "+"
+        strand = minus if any(s.reverse for s in m.spans) else plus
         biotype = ann.pop("type")
         name = ann.pop("name")
         db.add_feature(
diff --git a/src/cogent3/core/location.py b/src/cogent3/core/location.py
index 4f759c2c2..a90bcff5a 100644
--- a/src/cogent3/core/location.py
+++ b/src/cogent3/core/location.py
@@ -1,5 +1,6 @@
 import copy
 import dataclasses
+import enum
 import functools
 import inspect
 import json
@@ -7,7 +8,7 @@ from abc import ABC, abstractmethod
 from bisect import bisect_left, bisect_right
 from collections.abc import Iterator, Sequence
 from functools import total_ordering
-from typing import Any, NoReturn, Optional, Union
+from typing import Any, NoReturn, Union
 
 import numba
 import numpy
@@ -22,7 +23,29 @@ strip = str.strip
 
 _DEFAULT_GAP_DTYPE = numpy.int32
 
-OptInt = Optional[int]
+OptInt = int | None
+
+
+class Strand(enum.Enum):
+    """enum for defining strand information"""
+
+    PLUS = 1
+    MINUS = -1
+    NONE = None
+
+    @classmethod
+    def from_value(cls, value: str | int | None) -> "Strand":
+        if value in (-1, -1.0, "-", "-1", "minus", "Minus", "MINUS", cls.MINUS):
+            return cls.MINUS
+        return cls.NONE if value in (None, cls.NONE, 0, 0.0, False) else cls.PLUS
+
+    def __int__(self) -> int:
+        return self.value if self.value is not None else 0
+
+    def __str__(self) -> str:
+        if self.value is None:
+            return ""
+        return "-" if self.value == -1 else "+"
 
 
 def _norm_index(i, length, default):
@@ -492,9 +515,9 @@ class TerminalPadding(_LostSpan):
         return f"?{self.length}?"
 
 
-IntTypes = Union[int, numpy.int32, numpy.int64]
+IntTypes = int | numpy.int32 | numpy.int64
 IntArrayTypes = NDArray[int]
-SpanTypes = Union[Span, _LostSpan]
+SpanTypes = Span | _LostSpan
 SeqSpanTypes = Sequence[SpanTypes]
 SeqCoordTypes = Sequence[Sequence[IntTypes]]
 
diff --git a/src/cogent3/core/new_alignment.py b/src/cogent3/core/new_alignment.py
index adc86b7db..36492796b 100644
--- a/src/cogent3/core/new_alignment.py
+++ b/src/cogent3/core/new_alignment.py
@@ -40,6 +40,7 @@ from cogent3.core.info import Info as InfoClass
 from cogent3.core.location import (
     FeatureMap,
     IndelMap,
+    Strand,
 )
 from cogent3.core.profile import PSSM, MotifCountsArray, MotifFreqsArray, load_pssm
 from cogent3.maths.stats.number import CategoryCounter
@@ -1334,7 +1335,7 @@ class SequenceCollection:
         name: str,
         spans: list[tuple[int, int]],
         parent_id: OptStr = None,
-        strand: str = "+",
+        strand: str | int = "+",
     ) -> Feature:
         """
         add feature on named sequence
@@ -1363,7 +1364,7 @@ class SequenceCollection:
             raise ValueError(msg)
 
         feature = {k: v for k, v in locals().items() if k != "self"}
-
+        feature["strand"] = Strand.from_value(strand).value
         self.annotation_db.add_feature(**feature)
         feature.pop("parent_id", None)
         return self.make_feature(feature=feature)
@@ -5823,8 +5824,8 @@ class Alignment(SequenceCollection):
 
         feature["seqid"] = feature.get("seqid", None)
         # there's no sequence to bind to, the feature is directly on self
-        revd = feature.pop("strand", None) == "-"
-        feature["strand"] = "-" if revd else "+"
+        revd = Strand.from_value(feature.pop("strand", None)) is Strand.MINUS
+        feature["strand"] = Strand.MINUS.value if revd else Strand.PLUS.value
         fmap = FeatureMap.from_locations(
             locations=feature.pop("spans"),
             parent_length=len(self),
@@ -5891,7 +5892,7 @@ class Alignment(SequenceCollection):
             self.annotation_db = DEFAULT_ANNOTATION_DB()
 
         feature = {k: v for k, v in locals().items() if k != "self"}
-
+        feature["strand"] = Strand.from_value(strand).value
         self.annotation_db.add_feature(**feature)
         for discard in ("on_alignment", "parent_id"):
             feature.pop(discard, None)
@@ -6023,12 +6024,14 @@ class Alignment(SequenceCollection):
             if seq_map is None:
                 seq_map = self.seqs[0].map.to_feature_map()
                 *_, strand = self.seqs[0].seq.parent_coordinates()
+            else:
+                strand = feature.pop("strand", None)
 
             spans = numpy.array(feature["spans"])
             spans = seq_map.relative_position(spans)
             feature["spans"] = spans.tolist()
             # and if i've been reversed...?
-            feature["strand"] = "-" if strand == -1 else "+"
+            feature["strand"] = Strand.from_value(strand).value
             yield self.make_feature(feature=feature, on_alignment=on_al)
 
     def get_projected_feature(self, *, seqid: str, feature: Feature) -> Feature:
diff --git a/src/cogent3/core/new_sequence.py b/src/cogent3/core/new_sequence.py
index 8e103499e..59985961a 100644
--- a/src/cogent3/core/new_sequence.py
+++ b/src/cogent3/core/new_sequence.py
@@ -37,6 +37,7 @@ from cogent3.core.location import (
     FeatureMap,
     IndelMap,
     LostSpan,
+    Strand,
     _input_vals_neg_step,
     _input_vals_pos_step,
 )
@@ -1130,8 +1131,10 @@ class Sequence:
         feature = dict(feature)
         seq_rced = self._seq.is_reversed
         spans = feature.pop("spans", None)
-        revd = feature.pop("strand", None) == "-"
-        feature["strand"] = "+" if revd == seq_rced else "-"
+        revd = Strand.from_value(feature.pop("strand", None)) is Strand.MINUS
+        feature["strand"] = (
+            Strand.PLUS.value if revd == seq_rced else Strand.MINUS.value
+        )
 
         vals = array(spans)
         pre = abs(vals.min()) if vals.min() < 0 else 0
@@ -1234,9 +1237,12 @@ class Sequence:
         -------
         Feature instance
         """
+        local_vars = locals()
+        local_vars["strand"] = Strand.from_value(strand).value
+
         feature_data = FeatureDataType(
             seqid=self.name,
-            **{n: v for n, v in locals().items() if n not in ("self", "seqid")},
+            **{n: v for n, v in local_vars.items() if n not in ("self", "seqid")},
         )
         if self._annotation_db is None:
             self._init_annotation_db()
@@ -1837,7 +1843,7 @@ class Sequence:
         seqid, start, end, strand of this sequence on the parent. strand is either
         -1 or 1.
         """
-        strand = -1 if self._seq.is_reversed else 1
+        strand = Strand.MINUS.value if self._seq.is_reversed else Strand.PLUS.value
         return (
             self._seq.seqid,
             self._seq.slice_record.parent_start,


Test patch:
diff --git a/tests/test_core/test_annotation.py b/tests/test_core/test_annotation.py
index c69afcae5..6cd862292 100644
--- a/tests/test_core/test_annotation.py
+++ b/tests/test_core/test_annotation.py
@@ -30,7 +30,7 @@ def makeSampleAlignment():  # ported
     seq1 = makeSampleSequence("FAKE01")
     seq2 = makeSampleSequence("FAKE02", with_gaps=True)
     seqs = {seq1.name: seq1, seq2.name: seq2}
-    aln = make_aligned_seqs(data=seqs, array_align=False)
+    aln = make_aligned_seqs(data=seqs, moltype="dna", array_align=False)
     aln.add_feature(
         biotype="misc_feature",
         name="misc",
@@ -256,7 +256,7 @@ def test_aln_feature_to_dict():  # ported
         makeSampleSequence("s1", with_gaps=False),
         makeSampleSequence("s2", with_gaps=True),
     ]
-    aln = make_aligned_seqs(seqs, array_align=False)
+    aln = make_aligned_seqs(seqs, moltype="dna", array_align=False)
     feature_data = {
         "biotype": "CDS",
         "name": "fake",
@@ -285,42 +285,12 @@ def test_seq_slice_seqfeat_invalid(ann_aln):  # ported
 def test_gbdb_get_children_get_parent(DATA_DIR):  # ported
     seq = load_seq(DATA_DIR / "annotated_seq.gb")
     seq = seq[2900:6000]
-    (orig,) = list(seq.get_features(biotype="gene", name="CNA00110"))
-    (child,) = list(orig.get_children("CDS"))
+    orig, *_ = list(seq.get_features(biotype="gene", name="CNA00110"))
+    child, *_ = list(orig.get_children("CDS"))
     parent, *_ = list(child.get_parent())
     assert parent == orig
 
 
-@pytest.mark.parametrize("rev", [False, True])
-def test_features_survives_seq_rename(rev):  # ported
-    segments = ["A" * 10, "C" * 10, "T" * 5, "C" * 5, "A" * 5]
-
-    seq = DNA.make_seq(seq="".join(segments), name="original")
-    gene = seq.add_feature(biotype="gene", name="gene1", spans=[(10, 20), (25, 30)])
-    gene_expect = str(seq[10:20]) + str(seq[25:30])
-    assert str(gene.get_slice()) == gene_expect
-    domain = seq.add_feature(
-        biotype="domain",
-        name="domain1",
-        spans=[(20, 25)],
-        strand="-",
-    )
-    domain_expect = str(seq[20:25].rc())
-    domain_got = domain.get_slice()
-    assert str(domain_got) == domain_expect
-    sliced = seq[5:-3]
-    sliced.name = "sliced"
-    sliced = sliced.rc() if rev else sliced
-
-    got = next(iter(sliced.get_features(name="gene1")))
-    got = got.get_slice()
-    assert str(got) == gene_expect
-
-    got = next(iter(sliced.get_features(name="domain1")))
-    got = got.get_slice()
-    assert str(got) == domain_expect
-
-
 def make_aligned(**kwargs):
     return make_aligned_seqs(array_align=False, **kwargs)
 
diff --git a/tests/test_core/test_annotation_db.py b/tests/test_core/test_annotation_db.py
index fc74f8e9f..86da1dc0f 100644
--- a/tests/test_core/test_annotation_db.py
+++ b/tests/test_core/test_annotation_db.py
@@ -416,7 +416,7 @@ def test_feature_strand():
     minus_spans = [(4, 7), (11, 13)]
     minus_seq = "".join(raw_seq[s:e] for s, e in minus_spans)
     minus_seq = "".join([{"T": "A", "A": "T"}[b] for b in minus_seq[::-1]])
-    seq = make_seq(seq=raw_seq, name="s1", moltype="dna")
+    seq = make_seq(seq=raw_seq, name="s1", moltype="dna", new_type=True)
     db = GffAnnotationDb()
     db.add_feature(
         seqid="s1",
diff --git a/tests/test_core/test_location.py b/tests/test_core/test_location.py
index 1328e8c62..8f5a995a2 100644
--- a/tests/test_core/test_location.py
+++ b/tests/test_core/test_location.py
@@ -1,5 +1,3 @@
-"""Unit tests for Span classes."""
-
 import re
 from itertools import combinations
 from unittest import TestCase
@@ -14,6 +12,7 @@ from cogent3.core.location import (
     IndelMap,
     LostSpan,
     Span,
+    Strand,
     TerminalPadding,
     coords_intersect,
     coords_minus_coords,
@@ -1560,3 +1559,37 @@ def test_indel_map_merge_aligned_indices():
     )
 
     numpy.testing.assert_allclose(merged.array, exp.array)
+
+
+@pytest.mark.parametrize(
+    "minus", [-1, "-", "-1", -1.0, "Minus", "minus", "MINUS", Strand.MINUS]
+)
+def test_minus_strand(minus):
+    strand = Strand.from_value(minus)
+    assert strand is Strand.MINUS
+    assert int(strand) == -1
+    assert str(strand) == "-"
+
+
+@pytest.mark.parametrize("plus", [1, "+", "+1", "plus", "Plus", "PLUS", Strand.PLUS])
+def test_plus_strand(plus):
+    strand = Strand.from_value(plus)
+    assert strand is Strand.PLUS
+    assert int(strand) == 1
+    assert str(strand) == "+"
+
+
+@pytest.mark.parametrize("null", [None, 0, 0.0, Strand.NONE])
+def test_null_strand(null):
+    strand = Strand.from_value(null)
+    assert strand is Strand.NONE
+    assert int(strand) == 0
+    assert str(strand) == ""
+
+
+@pytest.mark.parametrize("invalid", ["unknown_strand_val", "foo"])
+def test_invalid_strand_defaults_to_plus(invalid):
+    strand = Strand.from_value(invalid)
+    assert strand is Strand.PLUS
+    assert int(strand) == 1
+    assert str(strand) == "+"
diff --git a/tests/test_core/test_new_aln_annotation.py b/tests/test_core/test_new_aln_annotation.py
index 77519d961..b02d151ca 100644
--- a/tests/test_core/test_new_aln_annotation.py
+++ b/tests/test_core/test_new_aln_annotation.py
@@ -273,7 +273,7 @@ def test_aln_feature_to_dict():
         "spans": [
             (5, 9),
         ],
-        "strand": "+",
+        "strand": 1,
         "seqid": None,
         "on_alignment": True,
     }
diff --git a/tests/test_core/test_new_seq_annotation.py b/tests/test_core/test_new_seq_annotation.py
index 545700809..7c0a1a4d3 100644
--- a/tests/test_core/test_new_seq_annotation.py
+++ b/tests/test_core/test_new_seq_annotation.py
@@ -34,7 +34,7 @@ def test_seq_feature_to_dict():
         "spans": [
             (5, 10),
         ],
-        "strand": "+",
+        "strand": 1,
         "seqid": "test_seq",
         "on_alignment": False,
     }
diff --git a/tests/test_parse/test_genbank.py b/tests/test_parse/test_genbank.py
index bb70f26c4..bd5889ea9 100644
--- a/tests/test_parse/test_genbank.py
+++ b/tests/test_parse/test_genbank.py
@@ -1,6 +1,7 @@
 """Unit tests for the GenBank database parsers."""
 
 import io
+import os
 import pathlib
 from unittest import TestCase
 
@@ -9,6 +10,7 @@ import pytest
 from cogent3.parse import genbank
 
 # ruff: noqa: SIM905
+NEW_TYPE = "COGENT3_NEW_TYPE" in os.environ
 
 
 class GenBankTests(TestCase):
@@ -445,6 +447,7 @@ def rich_gb():
         return next(s for l, s in parser)
 
 
+@pytest.mark.skipif(not NEW_TYPE, reason="different handling of strand")
 def test_rich_parser(rich_gb):
     """correctly constructs +/- strand features"""
     cds = {f.name: f for f in rich_gb.get_features(biotype="CDS")}