diff --git a/python-spec/src/somacore/query/axis.py b/python-spec/src/somacore/query/axis.py index 1b5a2cb3..e3088900 100644 --- a/python-spec/src/somacore/query/axis.py +++ b/python-spec/src/somacore/query/axis.py @@ -1,11 +1,11 @@ -from typing import Any, Optional, Sequence, Tuple +from typing import Optional, Sequence, Tuple import attrs import numpy as np import pyarrow as pa -from typing_extensions import TypeGuard from .. import options +from .. import types def _canonicalize_coords( @@ -22,7 +22,7 @@ def _canonicalize_coords( raise TypeError( f"query coordinates must be a sequence, not a single {type(in_coords)}" ) - if not _is_normal_sequence(in_coords): + if not types.is_nonstringy_sequence(in_coords): raise TypeError( "query coordinates must be a normal sequence, not `str` or `bytes`." ) @@ -42,10 +42,6 @@ def _canonicalize_coord(coord: options.SparseDFCoord) -> options.SparseDFCoord: raise TypeError(f"{type(coord)} object cannot be used as a coordinate.") -def _is_normal_sequence(it: Any) -> TypeGuard[Sequence]: - return not isinstance(it, (str, bytes)) and isinstance(it, Sequence) - - @attrs.define(frozen=True, kw_only=True) class AxisQuery: """Single-axis dataframe query with coordinates and a value filter. diff --git a/python-spec/src/somacore/types.py b/python-spec/src/somacore/types.py index bb5e86b9..62733978 100644 --- a/python-spec/src/somacore/types.py +++ b/python-spec/src/somacore/types.py @@ -1,7 +1,17 @@ """Type and interface declarations that are not specific to options.""" -from typing import Optional, TypeVar -from typing_extensions import Protocol, Self, runtime_checkable +from typing import Any, Optional, TypeVar, Sequence +from typing_extensions import Protocol, Self, runtime_checkable, TypeGuard + + +def is_nonstringy_sequence(it: Any) -> TypeGuard[Sequence]: + """Returns true if a sequence is a "normal" sequence and not str or bytes. + + str and bytes are "weird" sequences because iterating them gives you + another str or bytes instance for each character, and when used as a + sequence is not what users want. + """ + return not isinstance(it, (str, bytes)) and isinstance(it, Sequence) class Comparable(Protocol): diff --git a/python-spec/testing/test_types.py b/python-spec/testing/test_types.py new file mode 100644 index 00000000..3034e0c1 --- /dev/null +++ b/python-spec/testing/test_types.py @@ -0,0 +1,21 @@ +from typing import Any, Iterable +import unittest + +from somacore import types + + +class TestTypes(unittest.TestCase): + def test_is_nonstringy_sequence(self): + seqs: Iterable[Any] = ([], (), range(10)) + for seq in seqs: + with self.subTest(seq): + self.assertTrue(types.is_nonstringy_sequence(seq)) + + non_seqs: Iterable[Any] = (1, "hello", b"goodbye", (x for x in range(10))) + for non_seq in non_seqs: + with self.subTest(non_seq): + self.assertFalse(types.is_nonstringy_sequence(non_seq)) + + def test_slice(self): + self.assertIsInstance(slice(None), types.Slice) + self.assertNotIsInstance((1, 2), types.Slice)