diff --git a/python-spec/src/somacore/types.py b/python-spec/src/somacore/types.py index 62733978..edc4639b 100644 --- a/python-spec/src/somacore/types.py +++ b/python-spec/src/somacore/types.py @@ -1,7 +1,8 @@ """Type and interface declarations that are not specific to options.""" -from typing import Any, Optional, TypeVar, Sequence -from typing_extensions import Protocol, Self, runtime_checkable, TypeGuard +import sys +from typing import Any, NoReturn, Optional, Type, TypeVar, Sequence, TYPE_CHECKING +from typing_extensions import Protocol, Self, TypeGuard def is_nonstringy_sequence(it: Any) -> TypeGuard[Sequence]: @@ -36,25 +37,45 @@ def __gt__(self, __other: Self) -> bool: ... -_Cmp_co = TypeVar("_Cmp_co", bound=Comparable, covariant=True) +_T = TypeVar("_T") +_T_co = TypeVar("_T_co", covariant=True) -@runtime_checkable -class Slice(Protocol[_Cmp_co]): +class Slice(Protocol[_T_co]): """A slice which stores a certain type of object. This protocol describes the built in ``slice`` type, with a hint to callers - about what type they should put *inside* the slice. + about what type they should put *inside* the slice. It is for type + annotations only and is not runtime-checkable (i.e., you can't do + ``isinstance(thing, Slice)``), because ``range`` objects also have + ``start``/``stop``/``step`` and would match, but are *not* slices. """ @property - def start(self) -> Optional[_Cmp_co]: + def start(self) -> Optional[_T_co]: ... @property - def stop(self) -> Optional[_Cmp_co]: + def stop(self) -> Optional[_T_co]: ... @property - def step(self) -> Optional[_Cmp_co]: + def step(self) -> Optional[_T_co]: ... + + if sys.version_info < (3, 10) and not TYPE_CHECKING: + # Python 3.9 and below have a bug where any Protocol with an @property + # was always regarded as runtime-checkable. + @classmethod + def __subclasscheck__(cls, __subclass: type) -> NoReturn: + raise TypeError("Slice is not a runtime-checkable protocol") + + +def is_slice_of(__obj: object, __typ: Type[_T]) -> TypeGuard[Slice[_T]]: + return ( + # We only respect `slice`s proper. + isinstance(__obj, slice) + and (__obj.start is None or isinstance(__obj.start, __typ)) + and (__obj.stop is None or isinstance(__obj.stop, __typ)) + and (__obj.step is None or isinstance(__obj.step, __typ)) + ) diff --git a/python-spec/testing/test_types.py b/python-spec/testing/test_types.py index 3034e0c1..528a74ec 100644 --- a/python-spec/testing/test_types.py +++ b/python-spec/testing/test_types.py @@ -1,3 +1,4 @@ +import itertools from typing import Any, Iterable import unittest @@ -17,5 +18,34 @@ def test_is_nonstringy_sequence(self): self.assertFalse(types.is_nonstringy_sequence(non_seq)) def test_slice(self): - self.assertIsInstance(slice(None), types.Slice) - self.assertNotIsInstance((1, 2), types.Slice) + with self.assertRaises(TypeError): + issubclass(slice, types.Slice) # type: ignore[misc] + with self.assertRaises(TypeError): + isinstance(slice(None), types.Slice) # type: ignore[misc] + + def test_is_slice_of(self): + for sss_int in itertools.product((None, 1), (None, 1), (None, 1)): + slc_int = slice(*sss_int) # start, stop, step + with self.subTest(slc_int): + self.assertTrue(types.is_slice_of(slc_int, int)) + if slc_int != slice(None): + # Slices of one type are not slices of a disjoint type, + # except for the empty slice which is universal. + self.assertFalse(types.is_slice_of(slc_int, str)) + for sss_str in itertools.product((None, ""), (None, ""), (None, "")): + slc_str = slice(*sss_str) # start, stop, step + with self.subTest(slc_str): + self.assertTrue(types.is_slice_of(slc_str, str)) + if slc_str != slice(None): + self.assertFalse(types.is_slice_of(slc_str, int)) + + # Non-slices + self.assertFalse(types.is_slice_of(1, int)) + self.assertFalse(types.is_slice_of(range(10), int)) + + # All slots must match + slc_heterogeneous = slice("a", 1, ()) + self.assertFalse(types.is_slice_of(slc_heterogeneous, str)) + self.assertFalse(types.is_slice_of(slc_heterogeneous, int)) + self.assertFalse(types.is_slice_of(slc_heterogeneous, tuple)) + self.assertTrue(types.is_slice_of(slc_heterogeneous, object))