Skip to content

Commit

Permalink
Make Slice protocol not runtime_checkable. (#128)
Browse files Browse the repository at this point in the history
Since `range` has the members `start`/`stop`/`step`, that meant that
`isinstance(range(x), Slice)` would return True, when a `range` is *not*
a `Slice`.  Instead, we only provide the `is_slice_of` function
(originally from `tiledbsoma`) to perform the appropriate type check.

This also unrestricts `Slice`s from being `Comparable`, since the
built-in `slice` type does not have this restriction.
  • Loading branch information
thetorpedodog authored Feb 15, 2023
1 parent 582f12e commit 1482d48
Show file tree
Hide file tree
Showing 2 changed files with 62 additions and 11 deletions.
39 changes: 30 additions & 9 deletions python-spec/src/somacore/types.py
Original file line number Diff line number Diff line change
@@ -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]:
Expand Down Expand Up @@ -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))
)
34 changes: 32 additions & 2 deletions python-spec/testing/test_types.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import itertools
from typing import Any, Iterable
import unittest

Expand All @@ -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))

0 comments on commit 1482d48

Please sign in to comment.