Skip to content

Instantly share code, notes, and snippets.

@Guiorgy
Last active January 24, 2023 21:29
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Guiorgy/e9a4e671658472560d85e31a992c4c94 to your computer and use it in GitHub Desktop.
Save Guiorgy/e9a4e671658472560d85e31a992c4c94 to your computer and use it in GitHub Desktop.
A Python 3.11 implementation of a Read Only Span
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)))
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()
@Guiorgy
Copy link
Author

Guiorgy commented Jan 24, 2023

Other Python versions

  • If the Python version used is below 3.10, replace every usage of collections.abc with collections.

  • If the Python version used is between 3.7 and 3.11 (not including 3.11), then:

    • Add from __future__ import annotations to the top of the file
    • Remove Self from the import statements (from typing)
    • Replace every usage of Self with ReadOnlySpan
  • If the Python version used is below 3.7, then:

    • Remove Self from the import statements (from typing)
    • Replace every usage of Self with 'ReadOnlySpan' string

Tests

To run tests execute python -m unittest tests.py

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment