mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
Add a generic immutable sequence wrapper class
In anticipation of satisfying #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.
This commit is contained in:
committed by
Rodrigo Girão Serrão
parent
ad6a716d09
commit
2c827e18d0
65
src/textual/_collections.py
Normal file
65
src/textual/_collections.py
Normal file
@@ -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)
|
||||
81
tests/test_collections.py
Normal file
81
tests/test_collections.py
Normal file
@@ -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]
|
||||
Reference in New Issue
Block a user