8000 Make `Slice` protocol not `runtime_checkable`. (#128) · single-cell-data/SOMA@1482d48 · GitHub
[go: up one dir, main page]

Skip to content

Commit 1482d48

Browse files
Make Slice protocol not runtime_checkable. (#128)
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.
1 parent 582f12e commit 1482d48

File tree

2 files changed

+62
-11
lines changed

2 files changed

+62
-11
lines changed

python-spec/src/somacore/types.py

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
"""Type and interface declarations that are not specific to options."""
22

3-
from typing import Any, Optional, TypeVar, Sequence
4-
from typing_extensions import Protocol, Self, runtime_checkable, TypeGuard
3+
import sys
4+
from typing import Any, NoReturn, Optional, Type, TypeVar, Sequence, TYPE_CHECKING
5+
from typing_extensions import Protocol, Self, TypeGuard
56

67

78
def is_nonstringy_sequence(it: Any) -> TypeGuard[Sequence]:
@@ -36,25 +37,45 @@ def __gt__(self, __other: Self) -> bool:
3637
...
3738

3839

39-
_Cmp_co = TypeVar("_Cmp_co", bound=Comparable, covariant=True)
40+
_T = TypeVar("_T")
41+
_T_co = TypeVar("_T_co", covariant=True)
4042

4143

42-
@runtime_checkable
43-
class Slice(Protocol[_Cmp_co]):
44+
class Slice(Protocol[_T_co]):
4445
"""A slice which stores a certain type of object.
4546
4647
This protocol describes the built in ``slice`` type, with a hint to callers
47-
about what type they should put *inside* the slice.
48+
about what type they should put *inside* the slice. It is for type
49+
annotations only and is not runtime-checkable (i.e., you can't do
50+
``isinstance(thing, Slice)``), because ``range`` objects also have
51+
``start``/``stop``/``step`` and would match, but are *not* slices.
4852
"""
4953

5054
@property
51-
def start(self) -> Optional[_Cmp_co]:
55+
def start(self) -> Optional[_T_co]:
5256
...
5357

5458
@property
55-
def stop(self) -> Optional[_Cmp_co]:
59+
def stop(self) -> Optional[_T_co]:
5660
...
5761

5862
@property
59-
def step(self) -> Optional[_Cmp_co]:
63+
def step(self) -> Optional[_T_co]:
6064
...
65+
66+
if sys.version_info < (3, 10) and not TYPE_CHECKING:
67+
# Python 3.9 and below have a bug where any Protocol with an @property
68+
# was always regarded as runtime-checkable.
69+
@classmethod
70+
def __subclasscheck__(cls, __subclass: type) -> NoReturn:
71+
raise TypeError("Slice is not a runtime-checkable protocol")
72+
73+
74+
def is_slice_of(__obj: object, __typ: Type[_T]) -> TypeGuard[Slice[_T]]:
75+
return (
76+
# We only respect `slice`s proper.
77+
isinstance(__obj, slice)
78+
and (__obj.start is None or isinstance(__obj.start, __typ))
79+
and (__obj.stop is None or isinstance(__obj.stop, __typ))
80+
and (__obj.step is None or isinstance(__obj.step, __typ))
81+
)

python-spec/testing/test_types.py

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import itertools
12
from typing import Any, Iterable
23
import unittest
34

@@ -17,5 +18,34 @@ def test_is_nonstringy_sequence(self):
1718
self.assertFalse(types.is_nonstringy_sequence(non_seq))
1819

1920
def test_slice(self):
20-
self.assertIsInstance(slice(None), types.Slice)
21-
self.assertNotIsInstance((1, 2), types.Slice)
21+
with self.assertRaises(TypeError):
22+
issubclass(slice, types.Slice) # type: ignore[misc]
23+
with self.assertRaises(TypeError):
24+
isinstance(slice(None), types.Slice) # type: ignore[misc]
25+
26+
def test_is_slice_of(self):
8000 27+
for sss_int in itertools.product((None, 1), (None, 1), (None, 1)):
28+
slc_int = slice(*sss_int) # start, stop, step
29+
with self.subTest(slc_int):
30+
self.assertTrue(types.is_slice_of(slc_int, int))
31+
if slc_int != slice(None):
32+
# Slices of one type are not slices of a disjoint type,
33+
# except for the empty slice which is universal.
34+
self.assertFalse(types.is_slice_of(slc_int, str))
35+
for sss_str in itertools.product((None, ""), (None, ""), (None, "")):
36+
slc_str = slice(*sss_str) # start, stop, step
37+
with self.subTest(slc_str):
38+
self.assertTrue(types.is_slice_of(slc_str, str))
39+
if slc_str != slice(None):
40+
self.assertFalse(types.is_slice_of(slc_str, int))
41+
42+
# Non-slices
43+
self.assertFalse(types.is_slice_of(1, int))
44+
self.assertFalse(types.is_slice_of(range(10), int))
45+
46+
# All slots must match
47+
slc_heterogeneous = slice("a", 1, ())
48+
self.assertFalse(types.is_slice_of(slc_heterogeneous, str))
49+
self.assertFalse(types.is_slice_of(slc_heterogeneous, int))
50+
self.assertFalse(types.is_slice_of(slc_heterogeneous, tuple))
51+
self.assertTrue(types.is_slice_of(slc_heterogeneous, object))

0 commit comments

Comments
 (0)
0