forked from Textualize/textual
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add a generic immutable sequence wrapper class
In anticipation of satisfying Textualize#1398, this adds a generic immutable sequence wrapper class. The idea being that it can be used to wrap up a list or similar, that you don't want the caller to modify. This commit aims to get the basics down for this, and also adds a minimal set of unit tests.
- Loading branch information
Showing
2 changed files
with
146 additions
and
0 deletions.
There are no files selected for viewing
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
"""Provides collection-based utility code.""" | ||
|
||
from __future__ import annotations | ||
from typing import Generic, TypeVar, Iterator, overload, Iterable | ||
|
||
T = TypeVar("T") | ||
|
||
|
||
class ImmutableSequence(Generic[T]): | ||
"""Class to wrap a sequence of some sort, but not allow modification.""" | ||
|
||
def __init__(self, wrap: Iterable[T]) -> None: | ||
"""Initialise the immutable sequence. | ||
Args: | ||
wrap (Iterable[T]): The iterable value being wrapped. | ||
""" | ||
self._list = list(wrap) | ||
|
||
@overload | ||
def __getitem__(self, index: int) -> T: | ||
... | ||
|
||
@overload | ||
def __getitem__(self, index: slice) -> ImmutableSequence[T]: | ||
... | ||
|
||
def __getitem__(self, index: int | slice) -> T | ImmutableSequence[T]: | ||
return ( | ||
self._list[index] | ||
if isinstance(index, int) | ||
else ImmutableSequence[T](self._list[index]) | ||
) | ||
|
||
def __iter__(self) -> Iterator[T]: | ||
return iter(self._list) | ||
|
||
def __len__(self) -> int: | ||
return len(self._list) | ||
|
||
def __length_hint__(self) -> int: | ||
return len(self) | ||
|
||
def __bool__(self) -> bool: | ||
return bool(len(self)) | ||
|
||
def __contains__(self, item: T) -> bool: | ||
return item in self._list | ||
|
||
def index(self, item: T) -> int: | ||
"""Return the index of the given item. | ||
Args: | ||
item (T): The item to find in the sequence. | ||
Returns: | ||
int: The index of the item in the sequence. | ||
Raises: | ||
ValueError: If the item is not in the sequence. | ||
""" | ||
return self._list.index(item) | ||
|
||
def __reversed__(self) -> Iterator[T]: | ||
return reversed(self._list) |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
import pytest | ||
|
||
from typing import Iterable | ||
from textual._collections import ImmutableSequence | ||
|
||
def wrap(source: Iterable[int]) -> ImmutableSequence[int]: | ||
"""Wrap an itertable of integers inside an immutable sequence.""" | ||
return ImmutableSequence[int](source) | ||
|
||
|
||
def test_empty_immutable_sequence() -> None: | ||
"""An empty immutable sequence should act as anticipated.""" | ||
assert len(wrap([])) == 0 | ||
assert bool(wrap([])) is False | ||
assert list(wrap([])) == [] | ||
|
||
|
||
def test_non_empty_immutable_sequence() -> None: | ||
"""A non-empty immutable sequence should act as anticipated.""" | ||
assert len(wrap([0])) == 1 | ||
assert bool(wrap([0])) is True | ||
assert list(wrap([0])) == [0] | ||
|
||
|
||
def test_immutable_sequence_from_empty_iter() -> None: | ||
"""An immutable sequence around an empty iterator should act as anticipated.""" | ||
assert len(wrap([])) == 0 | ||
assert bool(wrap([])) is False | ||
assert list(wrap(iter([]))) == [] | ||
|
||
|
||
def test_immutable_sequence_from_non_empty_iter() -> None: | ||
"""An immutable sequence around a non-empty iterator should act as anticipated.""" | ||
assert len(wrap(range(23))) == 23 | ||
assert bool(wrap(range(23))) is True | ||
assert list(wrap(range(23))) == list(range(23)) | ||
|
||
|
||
def test_no_assign_to_immutable_sequence() -> None: | ||
"""It should not be possible to assign into an immutable sequence.""" | ||
tester = wrap([1,2,3,4,5]) | ||
with pytest.raises(TypeError): | ||
tester[0] = 23 | ||
with pytest.raises(TypeError): | ||
tester[0:3] = 23 | ||
|
||
|
||
def test_no_del_from_iummutable_sequence() -> None: | ||
"""It should not be possible delete an item from an immutable sequence.""" | ||
tester = wrap([1,2,3,4,5]) | ||
with pytest.raises(TypeError): | ||
del tester[0] | ||
|
||
|
||
def test_get_item_from_immutable_sequence() -> None: | ||
"""It should be possible to get an item from an immutable sequence.""" | ||
assert wrap(range(10))[0] == 0 | ||
assert wrap(range(10))[-1] == 9 | ||
|
||
def test_get_slice_from_immutable_sequence() -> None: | ||
"""It should be possible to get a slice from an immutable sequence.""" | ||
assert list(wrap(range(10))[0:2]) == [0,1] | ||
assert list(wrap(range(10))[0:-1]) == [0,1,2,3,4,5,6,7,8] | ||
|
||
|
||
def test_immutable_sequence_contains() -> None: | ||
"""It should be possible to see if an immutable sequence contains a value.""" | ||
tester = wrap([1,2,3,4,5]) | ||
assert 1 in tester | ||
assert 11 not in tester | ||
|
||
|
||
def test_immutable_sequence_index() -> None: | ||
tester = wrap([1,2,3,4,5]) | ||
assert tester.index(1) == 0 | ||
with pytest.raises(ValueError): | ||
_ = tester.index(11) | ||
|
||
|
||
def test_reverse_immutable_sequence() -> None: | ||
assert list(reversed(wrap([1,2]))) == [2,1] |