Skip to content

Commit

Permalink
Added more named list subclasses for the basic types.
Browse files Browse the repository at this point in the history
This supports a 1:1 mapping of the R atomic vectors with names.
  • Loading branch information
LTLA committed Jan 19, 2024
1 parent 5b9deb3 commit 9a64a65
Show file tree
Hide file tree
Showing 8 changed files with 570 additions and 2 deletions.
85 changes: 85 additions & 0 deletions src/biocutils/BooleanList.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
from typing import Any, Iterable, Optional, Sequence, Union

from .NamedList import NamedList
from .Names import Names
from .normalize_subscript import SubscriptTypes


def _coerce_to_bool(x: Any):
return None if x is None else bool(x)


class _SubscriptCoercer:
def __init__(self, data):
self._data = data

def __getitem__(self, index):
return _coerce_to_bool(self._data[index])


class BooleanList(NamedList):
"""
List of booleans. This mimics a regular Python list except that anything
added to it will be coerced into a boolean. None values are also acceptable
and are treated as missing booleans. The list may also be named (see
:py:class:`~NamedList`), which provides some dictionary-like functionality.
"""

def __init__(
self,
data: Optional[Iterable] = None,
names: Optional[Names] = None,
_validate: bool = True,
):
"""
Args:
data:
Some iterable object where all values can be coerced to booleans
or are None.
Alternatively this may itself be None, which defaults to an empty list.
names:
Names for the list elements, defaults to an empty list.
_validate:
Internal use only.
"""
if _validate:
if data is not None:
if isinstance(data, BooleanList):
data = data._data
else:
if isinstance(data, NamedList):
data = data._data
original = data
data = list(_coerce_to_bool(item) for item in original)
super().__init__(data, names, _validate=_validate)

def set_value(
self, index: Union[int, str], value: Any, in_place: bool = False
) -> "BooleanList":
"""Calls :py:meth:`~biocutils.NamedList.NamedList.set_value` after coercing ``value`` to a boolean."""
return super().set_value(index, _coerce_to_bool(value), in_place=in_place)

def set_slice(
self, index: SubscriptTypes, value: Sequence, in_place: bool = False
) -> "BooleanList":
"""Calls :py:meth:`~biocutils.NamedList.NamedList.set_slice` after coercing ``value`` to booleans."""
return super().set_slice(index, _SubscriptCoercer(value), in_place=in_place)

def safe_insert(
self, index: Union[int, str], value: Any, in_place: bool = False
) -> "BooleanList":
"""Calls :py:meth:`~biocutils.NamedList.NamedList.safe_insert` after coercing ``value`` to a boolean."""
return super().safe_insert(index, _coerce_to_bool(value), in_place=in_place)

def safe_append(self, value: Any, in_place: bool = False) -> "BooleanList":
"""Calls :py:meth:`~biocutils.NamedList.NamedList.safe_append` after coercing ``value`` to a boolean."""
return super().safe_append(_coerce_to_bool(value), in_place=in_place)

def safe_extend(self, other: Iterable, in_place: bool = True) -> "BooleanList":
"""Calls :py:meth:`~biocutils.NamedList.NamedList.safe_extend` after coercing elements of ``other`` to booleans."""
return super().safe_extend(
(_coerce_to_bool(y) for y in other), in_place=in_place
)
90 changes: 90 additions & 0 deletions src/biocutils/FloatList.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
from typing import Any, Iterable, Optional, Sequence, Union

from .NamedList import NamedList
from .Names import Names
from .normalize_subscript import SubscriptTypes


def _coerce_to_float(x: Any):
if x is None:
return None
try:
return float(x)
except:
return None


class _SubscriptCoercer:
def __init__(self, data):
self._data = data

def __getitem__(self, index):
return _coerce_to_float(self._data[index])


class FloatList(NamedList):
"""
List of floats. This mimics a regular Python list except that anything
added to it will be coerced into a float. None values are also acceptable
and are treated as missing floats. The list may also be named (see
:py:class:`~NamedList`), which provides some dictionary-like functionality.
"""

def __init__(
self,
data: Optional[Iterable] = None,
names: Optional[Names] = None,
_validate: bool = True,
):
"""
Args:
data:
Some iterable object where all values can be coerced to floats
or are None.
Alternatively this may itself be None, which defaults to an empty list.
names:
Names for the list elements, defaults to an empty list.
_validate:
Internal use only.
"""
if _validate:
if data is not None:
if isinstance(data, FloatList):
data = data._data
else:
if isinstance(data, NamedList):
data = data._data
original = data
data = list(_coerce_to_float(item) for item in original)
super().__init__(data, names, _validate=_validate)

def set_value(
self, index: Union[int, str], value: Any, in_place: bool = False
) -> "FloatList":
"""Calls :py:meth:`~biocutils.NamedList.NamedList.set_value` after coercing ``value`` to a float."""
return super().set_value(index, _coerce_to_float(value), in_place=in_place)

def set_slice(
self, index: SubscriptTypes, value: Sequence, in_place: bool = False
) -> "FloatList":
"""Calls :py:meth:`~biocutils.NamedList.NamedList.set_slice` after coercing ``value`` to floats."""
return super().set_slice(index, _SubscriptCoercer(value), in_place=in_place)

def safe_insert(
self, index: Union[int, str], value: Any, in_place: bool = False
) -> "FloatList":
"""Calls :py:meth:`~biocutils.NamedList.NamedList.safe_insert` after coercing ``value`` to a float."""
return super().safe_insert(index, _coerce_to_float(value), in_place=in_place)

def safe_append(self, value: Any, in_place: bool = False) -> "FloatList":
"""Calls :py:meth:`~biocutils.NamedList.NamedList.safe_append` after coercing ``value`` to a float."""
return super().safe_append(_coerce_to_float(value), in_place=in_place)

def safe_extend(self, other: Iterable, in_place: bool = True) -> "FloatList":
"""Calls :py:meth:`~biocutils.NamedList.NamedList.safe_extend` after coercing elements of ``other`` to floats."""
return super().safe_extend(
(_coerce_to_float(y) for y in other), in_place=in_place
)
90 changes: 90 additions & 0 deletions src/biocutils/IntegerList.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
from typing import Any, Iterable, Optional, Sequence, Union

from .NamedList import NamedList
from .Names import Names
from .normalize_subscript import SubscriptTypes


def _coerce_to_int(x: Any):
if x is None:
return None
try:
return int(x)
except:
return None


class _SubscriptCoercer:
def __init__(self, data):
self._data = data

def __getitem__(self, index):
return _coerce_to_int(self._data[index])


class IntegerList(NamedList):
"""
List of integers. This mimics a regular Python list except that anything
added to it will be coerced into a integer. None values are also acceptable
and are treated as missing integers. The list may also be named (see
:py:class:`~NamedList`), which provides some dictionary-like functionality.
"""

def __init__(
self,
data: Optional[Iterable] = None,
names: Optional[Names] = None,
_validate: bool = True,
):
"""
Args:
data:
Some iterable object where all values can be coerced to integers
or are None.
Alternatively this may itself be None, which defaults to an empty list.
names:
Names for the list elements, defaults to an empty list.
_validate:
Internal use only.
"""
if _validate:
if data is not None:
if isinstance(data, IntegerList):
data = data._data
else:
if isinstance(data, NamedList):
data = data._data
original = data
data = list(_coerce_to_int(item) for item in original)
super().__init__(data, names, _validate=_validate)

def set_value(
self, index: Union[int, str], value: Any, in_place: bool = False
) -> "IntegerList":
"""Calls :py:meth:`~biocutils.NamedList.NamedList.set_value` after coercing ``value`` to a integer."""
return super().set_value(index, _coerce_to_int(value), in_place=in_place)

def set_slice(
self, index: SubscriptTypes, value: Sequence, in_place: bool = False
) -> "IntegerList":
"""Calls :py:meth:`~biocutils.NamedList.NamedList.set_slice` after coercing ``value`` to integers."""
return super().set_slice(index, _SubscriptCoercer(value), in_place=in_place)

def safe_insert(
self, index: Union[int, str], value: Any, in_place: bool = False
) -> "IntegerList":
"""Calls :py:meth:`~biocutils.NamedList.NamedList.safe_insert` after coercing ``value`` to a integer."""
return super().safe_insert(index, _coerce_to_int(value), in_place=in_place)

def safe_append(self, value: Any, in_place: bool = False) -> "IntegerList":
"""Calls :py:meth:`~biocutils.NamedList.NamedList.safe_append` after coercing ``value`` to a integer."""
return super().safe_append(_coerce_to_int(value), in_place=in_place)

def safe_extend(self, other: Iterable, in_place: bool = True) -> "IntegerList":
"""Calls :py:meth:`~biocutils.NamedList.NamedList.safe_extend` after coercing elements of ``other`` to integers."""
return super().safe_extend(
(_coerce_to_int(y) for y in other), in_place=in_place
)
2 changes: 1 addition & 1 deletion src/biocutils/StringList.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from .normalize_subscript import SubscriptTypes


def _coerce_to_str(x: Any) -> bool:
def _coerce_to_str(x: Any):
return None if x is None else str(x)


Expand Down
5 changes: 4 additions & 1 deletion src/biocutils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@

from .Factor import Factor
from .StringList import StringList
from .IntegerList import IntegerList
from .FloatList import FloatList
from .BooleanList import BooleanList
from .Names import Names
from .NamedList import NamedList

Expand Down Expand Up @@ -54,4 +57,4 @@
from .convert_to_dense import convert_to_dense

from .get_height import get_height
from .is_high_dimensional import is_high_dimensional
from .is_high_dimensional import is_high_dimensional
100 changes: 100 additions & 0 deletions tests/test_BooleanList.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import biocutils
from biocutils import BooleanList, NamedList


def test_BooleanList_init():
x = BooleanList([ True, False, False, True ])
assert isinstance(x, BooleanList)
assert x.as_list() == [ True, False, False, True ]
assert x.get_names() is None

# Constructor works with other BooleanList objects.
recon = BooleanList(x)
assert recon.as_list() == x.as_list()

empty = BooleanList()
assert empty.as_list() == []

# Constructor works with Nones.
x = BooleanList([True,None,None,False])
assert x.as_list() == [ True, None, None, False ]

# Constructor works with other NamedList objects.
x = NamedList(["", 2, None, 0.0])
recon = BooleanList(x)
assert recon.as_list() == [False, True, None, False]


def test_BooleanList_getitem():
x = BooleanList([True, False, True, False ])

assert x[0]
sub = x[1:3]
assert isinstance(sub, BooleanList)
assert sub.as_list() == [False, True]

x.set_names(["A", "B", "C", "D"], in_place=True)
assert x["C"]
sub = x[["D", "C", "A", "B"]]
assert isinstance(sub, BooleanList)
assert sub.as_list() == [False, True, True, False]


def test_BooleanList_setitem():
x = BooleanList([False, True, True, False])
x[0] = None
assert x.as_list() == [None, True, True, False]
x[0] = 12345
assert x.as_list() == [True, True, True, False]

x[1:3] = [False, False]
assert x.as_list() == [True, False, False, False]

x[0:4:2] = [None, None]
assert x.as_list() == [None, False, None, False]

x.set_names(["A", "B", "C", "D"], in_place=True)
x["C"] = True
assert x.as_list() == [None, False, True, False]
x[["A", "B"]] = [False, True]
assert x.as_list() == [False, True, True, False]
x["E"] = "50"
assert x.as_list() == [False, True, True, False, True]
assert x.get_names().as_list() == [ "A", "B", "C", "D", "E" ]


def test_BooleanList_mutations():
# Insertion:
x = BooleanList([1,2,3,4])
x.insert(2, None)
x.insert(1, "")
assert x.as_list() == [ True, False, True, None, True, True ]

# Extension:
x.extend([None, 1, 5.0 ])
assert x.as_list() == [ True, False, True, None, True, True, None, True, True ]
alt = BooleanList([ 0, "", 1 ])
x.extend(alt)
assert x.as_list() == [ True, False, True, None, True, True, None, True, True, False, False, True ]

# Appending:
x.append(1)
assert x[-1]
x.append(None)
assert x[-1] is None


def test_BooleanList_generics():
x = BooleanList([False, False, True, True])
sub = biocutils.subset_sequence(x, [0,3,2,1])
assert isinstance(sub, BooleanList)
assert sub.as_list() == [False, True, True, False]

y = ["a", "b", "c", "d"]
com = biocutils.combine_sequences(x, y)
assert isinstance(com, BooleanList)
assert com.as_list() == [False, False, True, True, True, True, True, True]

ass = biocutils.assign_sequence(x, [1,3], ["a", 0])
assert isinstance(ass, BooleanList)
assert ass.as_list() == [False, True, True, False]
Loading

0 comments on commit 9a64a65

Please sign in to comment.