Last active
January 24, 2023 21:29
-
-
Save Guiorgy/e9a4e671658472560d85e31a992c4c94 to your computer and use it in GitHub Desktop.
A Python 3.11 implementation of a Read Only Span
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import collections.abc | |
from typing import overload, Sequence, TypeVar, Optional, Self | |
_T_co = TypeVar("_T_co", covariant=True) | |
class ReadOnlySpan(collections.abc.Sequence): | |
def __init__(self, source: Sequence[_T_co], start: Optional[int] = None, length: Optional[int] = None): | |
if not isinstance(source, collections.abc.Sequence): | |
raise TypeError | |
self._source = source | |
source_length = len(source) | |
if start is None: | |
self._start = 0 | |
elif 0 <= start < source_length: | |
self._start = start | |
else: | |
raise IndexError | |
if length is None: | |
self._length = source_length - self._start | |
elif 0 < length: | |
self._length = min(length, source_length - self._start) | |
else: | |
raise ValueError | |
def __len__(self) -> int: | |
return self._length | |
@overload | |
def __getitem__(self, index: int) -> _T_co: ... | |
@overload | |
def __getitem__(self, index: slice) -> Self: ... | |
def __getitem__(self, index): | |
if isinstance(index, int): | |
return self._source[self._start + (index if index >= 0 else self._length + index)] | |
elif isinstance(index, slice): | |
if index.step is not None and index.step != 1: | |
raise ValueError | |
if index.start is None: | |
start = 0 | |
elif index.start < 0: | |
start = index.start + self._length | |
if start < 0: | |
start = 0 | |
elif index.start >= self._length: | |
return ReadOnlySpan([]) | |
else: | |
start = index.start | |
if index.stop is None: | |
length = self._length - start | |
elif index.stop < 0: | |
length = index.stop + self._length - start | |
elif index.stop >= self._length: | |
length = self._length - start | |
else: | |
length = index.stop - start | |
if length <= 0: | |
return ReadOnlySpan([]) | |
return ReadOnlySpan(self._source, self._start + start, length) | |
else: | |
raise TypeError | |
def __iter__(self) -> Self: | |
self._it = iter(range(self._start, self._start + self._length)) | |
return self | |
def __next__(self) -> _T_co: | |
return self._source[next(self._it)] | |
def __repr__(self) -> str: | |
return '{}(start={}, length={}, data=[{}])'.format(self.__class__.__name__, self._start, self._length, ', '.join(str(self._source[i]) for i in range(self._start, self._start + self._length))) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import random | |
import unittest | |
from ReadOnlySpan import ReadOnlySpan | |
class TestReadOnlySpanInitialization(unittest.TestCase): | |
def __init__(self, *args, **kwargs): | |
super().__init__(*args, **kwargs) | |
self._range = range(10) | |
self._list = list(self._range) | |
self._tuple = tuple(self._range) | |
def _test_normal(self, ros, seq, start, length): | |
self.assertEqual(ros._source, seq) | |
self.assertEqual(ros._start, start) | |
self.assertEqual(ros._length, length) | |
def _test_start(self, ros, seq, start, length): | |
self.assertEqual(ros._source, seq) | |
self.assertEqual(ros._start, start) | |
self.assertEqual(ros._length, length) | |
def _test_length(self, ros, seq, start, length): | |
self.assertEqual(ros._source, seq) | |
self.assertEqual(ros._start, start) | |
self.assertEqual(ros._length, length) | |
def _test_start_and_length(self, ros, seq, start, length): | |
self.assertEqual(ros._source, seq) | |
self.assertEqual(ros._start, start) | |
self.assertEqual(ros._length, length) | |
def test_from_list_normal(self): | |
ros = ReadOnlySpan(self._list) | |
self._test_normal(ros, self._list, 0, 10) | |
def test_from_list_start(self): | |
ros = ReadOnlySpan(self._list, 5) | |
self._test_start(ros, self._list, 5, 5) | |
def test_from_list_length(self): | |
ros = ReadOnlySpan(self._list, length=5) | |
self._test_length(ros, self._list, 0, 5) | |
def test_from_list_start_and_length(self): | |
ros = ReadOnlySpan(self._list, 3, 5) | |
self._test_start_and_length(ros, self._list, 3, 5) | |
def test_from_list_start_out_of_bounds(self): | |
with self.assertRaises(IndexError): | |
ReadOnlySpan(self._list, -5) | |
with self.assertRaises(IndexError): | |
ReadOnlySpan(self._list, 15) | |
def test_from_list_length_out_of_bounds(self): | |
with self.assertRaises(ValueError): | |
ReadOnlySpan(self._list, length=-5) | |
with self.assertRaises(ValueError): | |
ReadOnlySpan(self._list, length=0) | |
ros = ReadOnlySpan(self._list, length=15) | |
self._test_length(ros, self._list, 0, 10) | |
def test_from_tuple_normal(self): | |
ros = ReadOnlySpan(self._tuple) | |
self._test_normal(ros, self._tuple, 0, 10) | |
def test_from_tuple_start(self): | |
ros = ReadOnlySpan(self._tuple, 5) | |
self._test_start(ros, self._tuple, 5, 5) | |
def test_from_tuple_length(self): | |
ros = ReadOnlySpan(self._tuple, length=5) | |
self._test_length(ros, self._tuple, 0, 5) | |
def test_from_tuple_start_and_length(self): | |
ros = ReadOnlySpan(self._tuple, 3, 5) | |
self._test_start_and_length(ros, self._tuple, 3, 5) | |
def test_from_tuple_start_out_of_bounds(self): | |
with self.assertRaises(IndexError): | |
ReadOnlySpan(self._tuple, -5) | |
with self.assertRaises(IndexError): | |
ReadOnlySpan(self._tuple, 15) | |
def test_from_tuple_length_out_of_bounds(self): | |
with self.assertRaises(ValueError): | |
ReadOnlySpan(self._tuple, length=-5) | |
with self.assertRaises(ValueError): | |
ReadOnlySpan(self._tuple, length=0) | |
ros = ReadOnlySpan(self._tuple, length=15) | |
self._test_length(ros, self._tuple, 0, 10) | |
def test_from_range_normal(self): | |
ros = ReadOnlySpan(self._range) | |
self._test_normal(ros, self._range, 0, 10) | |
def test_from_range_start(self): | |
ros = ReadOnlySpan(self._range, 5) | |
self._test_start(ros, self._range, 5, 5) | |
def test_from_range_length(self): | |
ros = ReadOnlySpan(self._range, length=5) | |
self._test_length(ros, self._range, 0, 5) | |
def test_from_range_start_and_length(self): | |
ros = ReadOnlySpan(self._range, 3, 5) | |
self._test_start_and_length(ros, self._range, 3, 5) | |
def test_from_range_start_out_of_bounds(self): | |
with self.assertRaises(IndexError): | |
ReadOnlySpan(self._range, -5) | |
with self.assertRaises(IndexError): | |
ReadOnlySpan(self._range, 15) | |
def test_from_range_length_out_of_bounds(self): | |
with self.assertRaises(ValueError): | |
ReadOnlySpan(self._range, length=-5) | |
with self.assertRaises(ValueError): | |
ReadOnlySpan(self._range, length=0) | |
ros = ReadOnlySpan(self._range, length=15) | |
self._test_length(ros, self._range, 0, 10) | |
def test_from_set_invalid(self): | |
with self.assertRaises(TypeError): | |
_set = set(self._range) | |
# noinspection PyTypeChecker | |
ReadOnlySpan(_set) | |
def test_from_empty_sequence(self): | |
_list = [] | |
ReadOnlySpan(_list) | |
_tuple = tuple(_list) | |
ReadOnlySpan(_tuple) | |
_range = range(0, 0) | |
ReadOnlySpan(_range) | |
class TestReadOnlySpanIndexAccess(unittest.TestCase): | |
def __init__(self, *args, **kwargs): | |
super().__init__(*args, **kwargs) | |
self._range = range(100) | |
self._list = list(self._range) | |
self._tuple = tuple(self._range) | |
def _test_sequence(self, seq): | |
seq_length = len(seq) | |
for test in range(10): | |
start = random.randint(0, seq_length - 1) | |
length = random.randint(1, seq_length - start) | |
ref = self._list[start:(start + length)] | |
try: | |
ros = ReadOnlySpan(seq, start, length) | |
self.assertEqual(len(ros), len(ref)) | |
for i in range(length): | |
self.assertEqual(ros[i], ref[i]) | |
except AssertionError: | |
print(f'(start={start}, length={length}, slice_reference={ref})') | |
raise | |
def test_list(self): | |
self._test_sequence(self._list) | |
def test_tuple(self): | |
self._test_sequence(self._tuple) | |
def test_range(self): | |
self._test_sequence(self._range) | |
def test_invalid_type(self): | |
with self.assertRaises(TypeError): | |
ros = ReadOnlySpan(self._list) | |
# noinspection PyTypeChecker, PyUnusedLocal | |
res = ros[1.5] | |
with self.assertRaises(TypeError): | |
ros = ReadOnlySpan(self._tuple) | |
# noinspection PyTypeChecker, PyUnusedLocal | |
res = ros[1.5] | |
with self.assertRaises(TypeError): | |
ros = ReadOnlySpan(self._range) | |
# noinspection PyTypeChecker, PyUnusedLocal | |
res = ros[1.5] | |
class TestReadOnlySpanIteration(unittest.TestCase): | |
def __init__(self, *args, **kwargs): | |
super().__init__(*args, **kwargs) | |
self._range = range(100) | |
self._list = list(self._range) | |
self._tuple = tuple(self._range) | |
def _test_sequence(self, seq): | |
seq_length = len(seq) | |
for test in range(10): | |
start = random.randint(0, seq_length - 1) | |
length = random.randint(1, seq_length - start) | |
ros = ReadOnlySpan(seq, start, length) | |
index = start | |
for elem in ros: | |
self.assertEqual(elem, seq[index]) | |
index += 1 | |
def test_list(self): | |
self._test_sequence(self._list) | |
def test_tuple(self): | |
self._test_sequence(self._tuple) | |
def test_range(self): | |
self._test_sequence(self._range) | |
class TestReadOnlySpanSlice(unittest.TestCase): | |
def __init__(self, *args, **kwargs): | |
super().__init__(*args, **kwargs) | |
self._list = list(range(100)) | |
self._ros = ReadOnlySpan(self._list) | |
def _assert_empty(self, ros): | |
self.assertEqual(len(ros), 0) | |
self.assertEqual(len(ros._source), 0) | |
def _assert_full(self, ros): | |
self.assertEqual(len(ros), len(self._list)) | |
for i in range(len(self._list)): | |
self.assertEqual(ros[i], self._list[i]) | |
def test_out_of_bounds(self): | |
with self.assertRaises(ValueError): | |
# noinspection PyUnusedLocal | |
ros = self._ros[::2] | |
with self.assertRaises(ValueError): | |
# noinspection PyUnusedLocal | |
ros = self._ros[::-2] | |
ros = self._ros[-150:] | |
self._assert_full(ros) | |
ros = self._ros[150:] | |
self._assert_empty(ros) | |
ros = self._ros[:-150] | |
self._assert_empty(ros) | |
ros = self._ros[:150] | |
self._assert_full(ros) | |
def test_full_slice(self): | |
ros = self._ros[:] | |
self._assert_full(ros) | |
ros = self._ros[::] | |
self._assert_full(ros) | |
ros = self._ros[0:100] | |
self._assert_full(ros) | |
ros = self._ros[0:100:1] | |
self._assert_full(ros) | |
ros = self._ros[-100:100] | |
self._assert_full(ros) | |
def test_stop_before_start(self): | |
ros = self._ros[90:10] | |
self._assert_empty(ros) | |
ros = self._ros[-10:10] | |
self._assert_empty(ros) | |
ros = self._ros[90:-90] | |
self._assert_empty(ros) | |
ros = self._ros[-10:-90] | |
self._assert_empty(ros) | |
def test_random(self): | |
seq_length = len(self._list) | |
for test in range(100): | |
start = random.randint(-seq_length - 10, seq_length + 10) | |
stop = random.randint(-seq_length - 10, seq_length + 10) | |
ref = self._list[start:stop] | |
try: | |
ros = self._ros[start:stop] | |
self.assertEqual(len(ros), len(ref)) | |
for i in range(0, len(ros)): | |
self.assertEqual(ros[i], ref[i]) | |
except AssertionError: | |
print(f'(start={start}, stop={stop}, slice_reference={ref})') | |
raise | |
if __name__ == '__main__': | |
unittest.main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Other Python versions
If the Python version used is below 3.10, replace every usage of
collections.abc
withcollections
.If the Python version used is between 3.7 and 3.11 (not including 3.11), then:
from __future__ import annotations
to the top of the fileSelf
from the import statements (fromtyping
)Self
withReadOnlySpan
If the Python version used is below 3.7, then:
Self
from the import statements (fromtyping
)Self
with'ReadOnlySpan'
stringTests
To run tests execute
python -m unittest tests.py