Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# Changelog

## Version 0.5.0
## Version 0.5.0 - 0.5.1

- Migate the package to `hatch`.
- Add index and list based methods to `Names` and `NamedList` classes.

## Version 0.4.0 - 0.4.2

Expand Down
76 changes: 75 additions & 1 deletion src/biocutils/NamedList.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from __future__ import annotations

from collections.abc import Iterable, Sequence
from collections.abc import Callable, Iterable, Sequence
from copy import deepcopy
from typing import Any

Expand Down Expand Up @@ -111,6 +111,21 @@ def __eq__(self, other: NamedList) -> bool:
return False
return self._data == other._data and self._names == other._names

def __contains__(self, value: Any) -> bool:
"""Return whether ``value`` is present in the list."""
return value in self._data

def index(self, value: Any, start: int = 0, stop: int | None = None) -> int:
"""Return the index of the first occurrence of ``value``."""
if stop is None:
return self._data.index(value, start)

return self._data.index(value, start, stop)

def count(self, value: Any) -> int:
"""Return the number of occurrences of ``value``."""
return self._data.count(value)

#################################
#####>>>> Get/set names <<<<#####
#################################
Expand Down Expand Up @@ -477,6 +492,65 @@ def __delitem__(self, index: int | str | slice):
"""Alias for :py:meth:`~delete`."""
self.delete(index)

def pop(self, index: int = -1) -> Any:
"""Remove and return the item at ``index``."""
value = self._data.pop(index)
if self._names is not None:
self._names.delete(index)

return value

def remove(self, value: Any) -> None:
"""Remove the first occurrence of ``value``."""
self.pop(self._data.index(value))

def clear(self) -> None:
"""Remove all items and names."""
self._data.clear()

if self._names is not None:
self._names.clear()

def reverse(self) -> None:
"""Reverse the items and their names in place."""
self._data.reverse()

if self._names is not None:
self._names.reverse()

def sort(self, *, key: Callable[[Any], Any] | None = None, reverse: bool = False) -> None:
"""Sort the items in place, keeping their names attached."""
if self._names is None:
self._data.sort(key=key, reverse=reverse)
return

paired = list(zip(self._names, self._data))
paired.sort(key=(lambda item: key(item[1])) if key is not None else (lambda item: item[1]), reverse=reverse)
self._names._wipe_reverse_index()
self._names._names[:] = [name for name, _ in paired]
self._data[:] = [value for _, value in paired]

def __mul__(self, n: int) -> NamedList:
"""Return a new ``NamedList`` containing ``n`` repetitions."""
names = None
if self._names is not None:
names = Names(self._names.as_list() * n, _validate=False)

return type(self)(self._data * n, names=names, _validate=False)

def __rmul__(self, n: int) -> NamedList:
"""Alias for :py:meth:`__mul__`."""
return self * n

def __imul__(self, n: int) -> NamedList:
"""Repeat the items and names in place ``n`` times."""
self._data *= n
if self._names is not None:
self._names._names *= n
self._names._wipe_reverse_index()

return self

#####################################
#####>>>> dict like methods <<<<#####
#####################################
Expand Down
51 changes: 51 additions & 0 deletions src/biocutils/Names.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,17 @@ def __contains__(self, name: str) -> bool:
"""
return self.map(name) >= 0

def index(self, value: str, start: int = 0, stop: int | None = None) -> int:
"""Return the index of the first matching name."""
if stop is None:
return self._names.index(value, start)

return self._names.index(value, start, stop)

def count(self, value: str) -> int:
"""Return the number of occurrences of ``value``."""
return self._names.count(value)

#################################
#####>>>> Get/set items <<<<#####
#################################
Expand Down Expand Up @@ -369,6 +380,46 @@ def __delitem__(self, index: int | slice):
"""Alias for :py:attr:`~delete`."""
self.delete(index)

def pop(self, index: int = -1) -> str:
"""Remove and return the name at ``index``."""
self._wipe_reverse_index()
return self._names.pop(index)

def remove(self, value: str) -> None:
"""Remove the first occurrence of ``value``."""
index = self._names.index(value)
self.pop(index)

def clear(self) -> None:
"""Remove all names."""
self._wipe_reverse_index()
self._names.clear()

def reverse(self) -> None:
"""Reverse the names in place."""
self._wipe_reverse_index()
self._names.reverse()

def sort(self, *, key: Callable[[str], Any] | None = None, reverse: bool = False) -> None:
"""Sort the names in place."""
self._wipe_reverse_index()
self._names.sort(key=key, reverse=reverse)

def __mul__(self, n: int) -> Names:
"""Return a new ``Names`` object containing ``n`` repetitions."""
return type(self)(self._names * n, _validate=False)

def __rmul__(self, n: int) -> Names:
"""Alias for :py:meth:`__mul__`."""
return self * n

def __imul__(self, n: int) -> Names: # noqa: PYI034
"""Repeat the names in place ``n`` times."""
self._wipe_reverse_index()
self._names *= n

return self

################################
#####>>>> Copy methods <<<<#####
################################
Expand Down
51 changes: 51 additions & 0 deletions tests/test_NamedList.py
Original file line number Diff line number Diff line change
Expand Up @@ -330,3 +330,54 @@ def test_NamedList_dict_methods():
assert list(y.keys()) == []
assert list(y.values()) == [10, 20]
assert list(y.items()) == [("0", 10), ("1", 20)]


def test_NamedList_list_methods():
x = NamedList([3, 1, 3, 2], names=["C", "A", "D", "B"])
assert 1 in x
assert 5 not in x
assert x.index(3) == 0
assert x.index(3, 1) == 2
assert x.index(3, 1, 3) == 2
assert x.count(3) == 2
with pytest.raises(ValueError):
x.index(5)

assert x.pop(1) == 1
assert x.as_list() == [3, 3, 2]
assert x.get_names().as_list() == ["C", "D", "B"]
x.remove(3)
assert x.as_list() == [3, 2]
assert x.get_names().as_list() == ["D", "B"]
assert x.pop() == 2
assert x.get_names().as_list() == ["D"]

x = NamedList([3, 1, 2], names=["C", "A", "B"])
x.get_names().map("A")
x.reverse()
assert x.as_list() == [2, 1, 3]
assert x.get_names().as_list() == ["B", "A", "C"]
assert x.get_names().map("A") == 1
x.sort()
assert x.as_list() == [1, 2, 3]
assert x.get_names().as_list() == ["A", "B", "C"]
x.sort(key=lambda value: -value)
assert x.as_list() == [3, 2, 1]
assert x.get_names().as_list() == ["C", "B", "A"]

assert (x * 2).as_list() == [3, 2, 1, 3, 2, 1]
assert (x * 2).get_names().as_list() == ["C", "B", "A", "C", "B", "A"]
assert (2 * x).get_names().as_list() == ["C", "B", "A", "C", "B", "A"]
x *= 2
assert x.as_list() == [3, 2, 1, 3, 2, 1]
assert x.get_names().as_list() == ["C", "B", "A", "C", "B", "A"]
x.clear()
assert x.as_list() == []
assert x.get_names().as_list() == []

unnamed = NamedList([1, 2])
unnamed.reverse()
unnamed.sort(reverse=True)
unnamed *= 0
assert unnamed.as_list() == []
assert unnamed.get_names() is None
31 changes: 31 additions & 0 deletions tests/test_Names.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,3 +257,34 @@ def test_Names_is_unique():

empty = Names([])
assert empty.is_unique


def test_Names_list_methods():
x = Names(["B", "A", "B", "C"])
assert x.index("B") == 0
assert x.index("B", 1) == 2
assert x.index("B", 1, 3) == 2
assert x.count("B") == 2
with pytest.raises(ValueError):
x.index("missing")

assert x.pop(1) == "A"
assert x.as_list() == ["B", "B", "C"]
assert x.map("C") == 2
x.remove("B")
assert x.as_list() == ["B", "C"]
assert x.pop() == "C"

x.reverse()
assert x.as_list() == ["B"]
assert (x * 2).as_list() == ["B", "B"]
assert (2 * x).as_list() == ["B", "B"]
x *= 2
assert x.as_list() == ["B", "B"]
x.extend(["A", "C"])
x.map("C")
x.sort(reverse=True)
assert x.as_list() == ["C", "B", "B", "A"]
assert x.map("C") == 0
x.clear()
assert x.as_list() == []
Loading