From 2521d10016341772db9f031bd7bd7f12d2afe1d2 Mon Sep 17 00:00:00 2001 From: Jayaram Kancherla Date: Mon, 2 Nov 2026 14:13:25 -0800 Subject: [PATCH 1/3] Add index and list based methods to `Names` and `NamedList` --- src/biocutils/NamedList.py | 76 +++++++++++++++++++++++++++++++++++++- src/biocutils/Names.py | 51 +++++++++++++++++++++++++ tests/test_NamedList.py | 51 +++++++++++++++++++++++++ tests/test_Names.py | 31 ++++++++++++++++ 4 files changed, 208 insertions(+), 1 deletion(-) diff --git a/src/biocutils/NamedList.py b/src/biocutils/NamedList.py index fc0cb95..f964c23 100644 --- a/src/biocutils/NamedList.py +++ b/src/biocutils/NamedList.py @@ -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 @@ -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 <<<<##### ################################# @@ -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 <<<<##### ##################################### diff --git a/src/biocutils/Names.py b/src/biocutils/Names.py index f869175..4d4110b 100644 --- a/src/biocutils/Names.py +++ b/src/biocutils/Names.py @@ -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 <<<<##### ################################# @@ -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 <<<<##### ################################ diff --git a/tests/test_NamedList.py b/tests/test_NamedList.py index dddc167..b8e82cb 100644 --- a/tests/test_NamedList.py +++ b/tests/test_NamedList.py @@ -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 diff --git a/tests/test_Names.py b/tests/test_Names.py index 43ce660..3d1a88b 100644 --- a/tests/test_Names.py +++ b/tests/test_Names.py @@ -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() == [] From d248734d03884726a6374bb487c4919450f56b5a Mon Sep 17 00:00:00 2001 From: Jayaram Kancherla Date: Mon, 2 Nov 2026 14:14:35 -0800 Subject: [PATCH 2/3] update changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 719fe2c..ad8a57a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 From 20df0f8c595cafebf1235649e4f12d062002839c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 2 Nov 2026 22:15:22 +0000 Subject: [PATCH 3/3] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad8a57a..1d82879 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ ## Version 0.5.0 - 0.5.1 - Migate the package to `hatch`. -- Add index and list based methods to `Names` and `NamedList` classes. +- Add index and list based methods to `Names` and `NamedList` classes. ## Version 0.4.0 - 0.4.2