implement sized chunks, related to #6
This commit is contained in:
parent
49d68cefa5
commit
320ff92c7d
127
src/dopt_basics/batching.py
Normal file
127
src/dopt_basics/batching.py
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Collection
|
||||||
|
from typing import Generic, Never, TypeVar
|
||||||
|
|
||||||
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
|
||||||
|
class Chunk(Generic[T]):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
capacity: int,
|
||||||
|
overfill_once: bool = True,
|
||||||
|
cap_property: str | None = None,
|
||||||
|
) -> None:
|
||||||
|
self.capacity = capacity
|
||||||
|
self.overfill = overfill_once
|
||||||
|
self.capacity_left: int = capacity
|
||||||
|
self.capacity_held: int = 0
|
||||||
|
self.full: bool = False
|
||||||
|
self._contents: list[T] = []
|
||||||
|
self.cap_property = cap_property
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return self._contents.__repr__()
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return self._contents.__str__()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def contents(self) -> list[T]:
|
||||||
|
return self._contents
|
||||||
|
|
||||||
|
def __len__(self) -> int:
|
||||||
|
return len(self._contents)
|
||||||
|
|
||||||
|
def __getitem__(
|
||||||
|
self,
|
||||||
|
idx: int,
|
||||||
|
) -> T:
|
||||||
|
return self._contents[idx]
|
||||||
|
|
||||||
|
def __raise_for_exceeded_capacity(self) -> Never:
|
||||||
|
raise ValueError("Item can not be added due to capacity limit.")
|
||||||
|
|
||||||
|
def _capacity_sufficient(
|
||||||
|
self,
|
||||||
|
size_to_add: int,
|
||||||
|
) -> bool:
|
||||||
|
if self.full:
|
||||||
|
return False
|
||||||
|
|
||||||
|
new_size = len(self) + size_to_add
|
||||||
|
|
||||||
|
if self.overfill and (new_size > self.capacity):
|
||||||
|
return True
|
||||||
|
elif new_size > self.capacity:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _recalc_capacity(
|
||||||
|
self,
|
||||||
|
size_to_add: int,
|
||||||
|
) -> None:
|
||||||
|
self.capacity_held += size_to_add
|
||||||
|
self.capacity_left = max(self.capacity_left - size_to_add, 0)
|
||||||
|
if self.capacity_left == 0:
|
||||||
|
self.full = True
|
||||||
|
else:
|
||||||
|
self.full = False
|
||||||
|
|
||||||
|
def append(
|
||||||
|
self,
|
||||||
|
object_: T,
|
||||||
|
) -> None:
|
||||||
|
size_to_add: int = 1
|
||||||
|
if self.cap_property is not None:
|
||||||
|
if not hasattr(object_, self.cap_property):
|
||||||
|
raise AttributeError("Object does not posses the wanted property")
|
||||||
|
attr = getattr(object_, self.cap_property)
|
||||||
|
if not isinstance(attr, int):
|
||||||
|
raise TypeError("Capacity property must be an integer")
|
||||||
|
size_to_add = attr
|
||||||
|
|
||||||
|
if self._capacity_sufficient(size_to_add):
|
||||||
|
self._contents.append(object_)
|
||||||
|
self._recalc_capacity(size_to_add)
|
||||||
|
else:
|
||||||
|
self.__raise_for_exceeded_capacity()
|
||||||
|
|
||||||
|
def extend(
|
||||||
|
self,
|
||||||
|
collection: Collection[T],
|
||||||
|
) -> None:
|
||||||
|
size_to_add: int = len(collection)
|
||||||
|
if self.cap_property is not None:
|
||||||
|
if not all(hasattr(object_, self.cap_property) for object_ in collection):
|
||||||
|
raise AttributeError("Not all objects posses the wanted property")
|
||||||
|
if not all(
|
||||||
|
isinstance(getattr(object_, self.cap_property), int) for object_ in collection
|
||||||
|
):
|
||||||
|
raise TypeError("Capacity property must be an integer")
|
||||||
|
size_to_add = sum(getattr(object_, self.cap_property) for object_ in collection)
|
||||||
|
|
||||||
|
if self._capacity_sufficient(size_to_add):
|
||||||
|
self._contents.extend(collection)
|
||||||
|
self._recalc_capacity(size_to_add)
|
||||||
|
else:
|
||||||
|
self.__raise_for_exceeded_capacity()
|
||||||
|
|
||||||
|
def remove(
|
||||||
|
self,
|
||||||
|
object_: T,
|
||||||
|
) -> None:
|
||||||
|
size_to_add: int = 1
|
||||||
|
if self.cap_property is not None:
|
||||||
|
if not hasattr(object_, self.cap_property):
|
||||||
|
raise AttributeError("Object does not posses the wanted property")
|
||||||
|
attr = getattr(object_, self.cap_property)
|
||||||
|
if not isinstance(attr, int):
|
||||||
|
raise TypeError("Capacity property must be an integer")
|
||||||
|
size_to_add = attr
|
||||||
|
size_to_remove = (-1) * size_to_add
|
||||||
|
|
||||||
|
self._contents.remove(object_)
|
||||||
|
self._recalc_capacity(size_to_remove)
|
||||||
142
tests/test_batching.py
Normal file
142
tests/test_batching.py
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from dopt_basics.batching import Chunk
|
||||||
|
|
||||||
|
|
||||||
|
class _Item:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
size: int,
|
||||||
|
) -> None:
|
||||||
|
self.size = size
|
||||||
|
|
||||||
|
|
||||||
|
def test_Chunk_base():
|
||||||
|
chunk: Chunk[int] = Chunk(3)
|
||||||
|
|
||||||
|
assert chunk.capacity == 3
|
||||||
|
assert chunk.capacity_held == 0
|
||||||
|
assert chunk.capacity_left == 3
|
||||||
|
assert chunk.full is False
|
||||||
|
assert len(chunk) == 0
|
||||||
|
assert not chunk.contents
|
||||||
|
|
||||||
|
|
||||||
|
# TODO implement behaviour
|
||||||
|
# def test_Chunk_base_overfill_append():
|
||||||
|
# chunk: Chunk[int] = Chunk(3)
|
||||||
|
|
||||||
|
# chunk.append(1)
|
||||||
|
# chunk.append(2)
|
||||||
|
# chunk.append(3)
|
||||||
|
# chunk.append(4) # allow once, then not again
|
||||||
|
|
||||||
|
|
||||||
|
def test_Chunk_base_no_overfill_append():
|
||||||
|
chunk: Chunk[int] = Chunk(3, overfill_once=False)
|
||||||
|
|
||||||
|
chunk.append(1)
|
||||||
|
assert chunk[0] == 1
|
||||||
|
chunk.append(2)
|
||||||
|
chunk.append(3)
|
||||||
|
with pytest.raises(ValueError, match="Item can not be added due to capacity limit"):
|
||||||
|
chunk.append(4)
|
||||||
|
|
||||||
|
|
||||||
|
def test_Chunk_base_overfill_extend():
|
||||||
|
chunk: Chunk[int] = Chunk(3)
|
||||||
|
|
||||||
|
chunk.extend((1, 2))
|
||||||
|
chunk.extend((3, 4))
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="Item can not be added due to capacity limit"):
|
||||||
|
chunk.extend((5, 6))
|
||||||
|
|
||||||
|
|
||||||
|
def test_Chunk_base_no_overfill_extend():
|
||||||
|
chunk: Chunk[int] = Chunk(3, overfill_once=False)
|
||||||
|
|
||||||
|
chunk.extend((1, 2))
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="Item can not be added due to capacity limit"):
|
||||||
|
chunk.extend((3, 4))
|
||||||
|
|
||||||
|
|
||||||
|
def test_Chunk_base_remove():
|
||||||
|
chunk: Chunk[int] = Chunk(3, overfill_once=False)
|
||||||
|
|
||||||
|
chunk.extend((1, 2))
|
||||||
|
assert chunk.capacity_held == 2
|
||||||
|
assert chunk.capacity_left == 1
|
||||||
|
|
||||||
|
chunk.remove(2)
|
||||||
|
assert chunk.capacity_held == 1
|
||||||
|
assert chunk.capacity_left == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_Chunk_property_append_success():
|
||||||
|
CAP_PROP = "size"
|
||||||
|
chunk: Chunk[_Item] = Chunk(3, overfill_once=False, cap_property=CAP_PROP)
|
||||||
|
i1 = _Item(2)
|
||||||
|
i2 = _Item(4)
|
||||||
|
|
||||||
|
chunk.append(i1)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="Item can not be added due to capacity limit"):
|
||||||
|
chunk.append(i2)
|
||||||
|
|
||||||
|
|
||||||
|
def test_Chunk_property_append_fail_no_attribute():
|
||||||
|
CAP_PROP = "size"
|
||||||
|
chunk: Chunk = Chunk(3, overfill_once=False, cap_property=CAP_PROP)
|
||||||
|
|
||||||
|
with pytest.raises(AttributeError, match="Object does not posses the wanted property"):
|
||||||
|
chunk.append(1)
|
||||||
|
|
||||||
|
|
||||||
|
def test_Chunk_property_append_fail_wrong_type():
|
||||||
|
CAP_PROP = "size"
|
||||||
|
chunk: Chunk = Chunk(3, overfill_once=False, cap_property=CAP_PROP)
|
||||||
|
i1 = _Item(4)
|
||||||
|
i1.size = "wrong type" # type: ignore
|
||||||
|
|
||||||
|
with pytest.raises(TypeError, match="Capacity property must be an integer"):
|
||||||
|
chunk.append(i1)
|
||||||
|
|
||||||
|
|
||||||
|
def test_Chunk_property_extend_fail_no_attribute():
|
||||||
|
CAP_PROP = "size"
|
||||||
|
chunk: Chunk = Chunk(3, overfill_once=False, cap_property=CAP_PROP)
|
||||||
|
i1 = _Item(2)
|
||||||
|
|
||||||
|
with pytest.raises(AttributeError, match="Not all objects posses the wanted property"):
|
||||||
|
chunk.extend((i1, 2))
|
||||||
|
|
||||||
|
|
||||||
|
def test_Chunk_property_extend_fail_wrong_type():
|
||||||
|
CAP_PROP = "size"
|
||||||
|
chunk: Chunk = Chunk(3, overfill_once=False, cap_property=CAP_PROP)
|
||||||
|
i1 = _Item(2)
|
||||||
|
i2 = _Item(4)
|
||||||
|
i2.size = "wrong type" # type: ignore
|
||||||
|
|
||||||
|
with pytest.raises(TypeError, match="Capacity property must be an integer"):
|
||||||
|
chunk.extend((i1, i2))
|
||||||
|
|
||||||
|
|
||||||
|
def test_Chunk_property_remove_fail_no_attribute():
|
||||||
|
CAP_PROP = "size"
|
||||||
|
chunk: Chunk = Chunk(3, overfill_once=False, cap_property=CAP_PROP)
|
||||||
|
|
||||||
|
with pytest.raises(AttributeError, match="Object does not posses the wanted property"):
|
||||||
|
chunk.remove(1)
|
||||||
|
|
||||||
|
|
||||||
|
def test_Chunk_property_remove_fail_wrong_type():
|
||||||
|
CAP_PROP = "size"
|
||||||
|
chunk: Chunk = Chunk(3, overfill_once=False, cap_property=CAP_PROP)
|
||||||
|
i1 = _Item(4)
|
||||||
|
i1.size = "wrong type" # type: ignore
|
||||||
|
|
||||||
|
with pytest.raises(TypeError, match="Capacity property must be an integer"):
|
||||||
|
chunk.remove(i1)
|
||||||
Loading…
x
Reference in New Issue
Block a user