diff --git a/src/biocutils/NamedList.py b/src/biocutils/NamedList.py index 3864227..b1ed911 100644 --- a/src/biocutils/NamedList.py +++ b/src/biocutils/NamedList.py @@ -41,6 +41,10 @@ def __init__(self, data: Optional[Iterable] = None, names: Optional[Names] = Non self._data = data self._names = names + ################################### + #####>>>> Bits and pieces <<<<##### + ################################### + def __len__(self) -> int: """ Returns: @@ -79,42 +83,11 @@ def __eq__(self, other: "NamedList") -> bool: Whether the current object is equal to ``other``, i.e., same data and names. """ - return self.get_data() == other.get_data() and self.get_names() == other.get_names() - - def get_data(self) -> list: - """ - Returns: - The underlying list of elements. - """ - return self._data - - @property - def data(self) -> list: - """Alias for :py:attr:`~get_data`.""" - return self.get_data() - - def set_data(self, data: Sequence, in_place: bool = False) -> "NamedList": - """ - Args: - data: - Replacement list of elements. This should have the same length - as the current object. - - in_place: - Whether to modify the current object in place. + return self._data == other._data and self._names == other._names - Returns: - A modified ``NamedList``, either as a new object or a reference to - the current object. - """ - if len(data) != len(self): - raise ValueError("replacement 'data' must be of the same length") - if in_place: - output = self - else: - output = self.copy() - output._data = data - return output + ################################# + #####>>>> Get/set names <<<<##### + ################################# def get_names(self) -> Names: """ @@ -128,6 +101,9 @@ def names(self) -> Names: """Alias for :py:attr:`~get_names`.""" return self.get_names() + def _shallow_copy(self): + return type(self)(self._data, self._names, _validate=False) + def set_names(self, names: Optional[Names], in_place: bool = False) -> "NamedList": """ Args: @@ -145,10 +121,14 @@ def set_names(self, names: Optional[Names], in_place: bool = False) -> "NamedLis if in_place: output = self else: - output = self.copy() + output = self._shallow_copy() output._names = _sanitize_names(names, len(self)) return output + ################################# + #####>>>> Get/set items <<<<##### + ################################# + def get_value(self, index: Union[str, int]) -> Any: """ Args: @@ -222,7 +202,7 @@ def set_value(self, index: Union[str, int], value: Any, in_place: bool = False) if in_place: output = self else: - output = self.copy() + output = self._shallow_copy() output._data = output._data.copy() if isinstance(index, str): @@ -276,7 +256,7 @@ def set_slice(self, index: SubscriptTypes, value: Sequence, in_place: bool = Fal if in_place: output = self else: - output = self.copy() + output = self._shallow_copy() output._data = output._data.copy() if scalar: output._data[index[0]] = value @@ -302,14 +282,15 @@ def __setitem__(self, index: SubscriptTypes, value: Any): else: self.set_slice(NormalizedSubscript(index), value, in_place=True) + ################################ + #####>>>> List methods <<<<##### + ################################ + def _define_output(self, in_place: bool) -> "NamedList": if in_place: return self - newdata = self._data.copy() - newnames = None - if self._names is not None: - newnames = self._names.copy() - return type(self)(newdata, names=newnames, _validate=False) + else: + return self.copy() def safe_insert(self, index: Union[int, str], value: Any, in_place: bool = False) -> "NamedList": """ @@ -411,12 +392,26 @@ def __iadd__(self, other: list): self.extend(other) return self + ################################ + #####>>>> Copy methods <<<<##### + ################################ + def copy(self) -> "NamedList": """ Returns: - A shallow copy of a ``NamedList`` with the same contents. + A shallow copy of a ``NamedList`` with the same contents. This + will copy the underlying list (and names, if any exist) so that any + in-place operations like :py:attr:`~append`, etc., on the new + object will not change the original object. """ - return type(self)(self._data, names=self._names, _validate=False) + newnames = self._names + if newnames is not None: + newnames = newnames.copy() + return type(self)(self._data.copy(), names=newnames, _validate=False) + + def __copy__(self) -> "NamedList": + """Alias for :py:meth:`~copy`.""" + return self.copy() def __deepcopy__(self, memo=None, _nil=[]) -> "NamedList": """ @@ -432,6 +427,17 @@ def __deepcopy__(self, memo=None, _nil=[]) -> "NamedList": """ return type(self)(deepcopy(self._data, memo, _nil), names=deepcopy(self._names, memo, _nil), _validate=False) + ############################ + #####>>>> Coercion <<<<##### + ############################ + + def as_list(self) -> list: + """ + Returns: + The underlying list of elements. + """ + return self._data + def as_dict(self) -> Dict[str, Any]: """ Returns: @@ -444,8 +450,27 @@ def as_dict(self) -> Dict[str, Any]: output[n] = self[i] return output + @staticmethod + def from_list(x: list) -> "NamedList": + """ + Args: + x: List of data elements. + + Returns: + A ``NamedList`` instance with the contents of ``x`` and no names. + """ + return NamedList(x) + @staticmethod def from_dict(x: dict) -> "NamedList": + """ + Args: + x: Dictionary where keys are strings (or can be coerced to them). + + Returns: + A ``NamedList`` instance where the list elements are the values of + ``x`` and the names are the stringified keys. + """ return NamedList(list(x.values()), names=Names(str(y) for y in x.keys())) @@ -456,7 +481,7 @@ def _subset_sequence_NamedList(x: NamedList, indices: Sequence[int]) -> NamedLis @combine_sequences.register def _combine_sequences_NamedList(*x: NamedList) -> NamedList: - output = x[0]._define_output(in_place=False) + output = x[0].copy() for i in range(1, len(x)): output.extend(x[i]) return output diff --git a/tests/test_Factor.py b/tests/test_Factor.py index 1528e85..dcccdcd 100644 --- a/tests/test_Factor.py +++ b/tests/test_Factor.py @@ -9,7 +9,7 @@ def test_Factor_init(): assert len(f) == 6 assert list(f) == ["A", "B", "C", "A", "C", "E"] assert list(f.get_codes()) == [0, 1, 2, 0, 2, 4] - assert f.get_levels().get_data() == ["A", "B", "C", "D", "E"] + assert f.get_levels().as_list() == ["A", "B", "C", "D", "E"] assert not f.get_ordered() # Works with missing values. @@ -162,30 +162,30 @@ def test_Factor_setitem(): def test_Factor_drop_unused_levels(): f = Factor([0, 1, 2, 0, 2, 4], levels=["A", "B", "C", "D", "E"]) f2 = f.drop_unused_levels() - assert f2.get_levels().get_data() == ["A", "B", "C", "E"] + assert f2.get_levels().as_list() == ["A", "B", "C", "E"] assert list(f2) == list(f) f = Factor([3, 4, 2, 3, 2, 4], levels=["A", "B", "C", "D", "E"]) f2 = f.drop_unused_levels(in_place=True) - assert f2.get_levels().get_data() == ["C", "D", "E"] + assert f2.get_levels().as_list() == ["C", "D", "E"] assert list(f2) == ["D", "E", "C", "D", "C", "E"] def test_Factor_set_levels(): f = Factor([0, 1, 2, 0, 2, 4], levels=["A", "B", "C", "D", "E"]) f2 = f.set_levels(["E", "D", "C", "B", "A"]) - assert f2.get_levels().get_data() == ["E", "D", "C", "B", "A"] + assert f2.get_levels().as_list() == ["E", "D", "C", "B", "A"] assert list(f2.get_codes()) == [4, 3, 2, 4, 2, 0] assert list(f2) == list(f) f = Factor([0, 1, 2, 0, 2, 4], levels=["A", "B", "C", "D", "E"]) f2 = f.set_levels(["E", "C", "A"], in_place=True) - assert f2.get_levels().get_data() == ["E", "C", "A"] + assert f2.get_levels().as_list() == ["E", "C", "A"] assert list(f2.get_codes()) == [2, -1, 1, 2, 1, 0] f = Factor([0, 1, 2, 0, 2, 4], levels=["A", "B", "C", "D", "E"]) f2 = f.set_levels("E") # reorders - assert f2.get_levels().get_data() == ["E", "A", "B", "C", "D"] + assert f2.get_levels().as_list() == ["E", "A", "B", "C", "D"] assert list(f2.get_codes()) == [1, 2, 3, 1, 3, 0] with pytest.raises(ValueError) as ex: @@ -240,7 +240,7 @@ def test_Factor_combine(): f1 = Factor([0, 2, 4, 2, 0], levels=["A", "B", "C", "D", "E"]) f2 = Factor([1, 3, 1], levels=["D", "E", "F", "G"]) out = combine(f1, f2) - assert out.get_levels().get_data() == ["A", "B", "C", "D", "E", "F", "G"] + assert out.get_levels().as_list() == ["A", "B", "C", "D", "E", "F", "G"] assert list(out.get_codes()) == [0, 2, 4, 2, 0, 4, 6, 4] f2 = Factor([1, 3, None], levels=["D", "E", "F", "G"]) diff --git a/tests/test_NamedList.py b/tests/test_NamedList.py index dc1d201..35c3bb3 100644 --- a/tests/test_NamedList.py +++ b/tests/test_NamedList.py @@ -7,21 +7,21 @@ def test_NamedList_init(): x = NamedList([1,2,3,4], names=['a', 'b', 'c', 'd']) assert isinstance(x, NamedList) - assert x.get_data() == [ 1,2,3,4 ] + assert x.as_list() == [ 1,2,3,4 ] assert x.get_names().as_list() == ["a", "b", "c", "d"] assert len(x) == 4 y = NamedList(x) - assert y.get_data() == [1,2,3,4] + assert y.as_list() == [1,2,3,4] assert y.get_names() is None # names are not carried over; this is intended, and not a bug. empty = NamedList() - assert empty.get_data() == [] + assert empty.as_list() == [] assert empty.get_names() is None assert len(empty) == 0 x = NamedList([1,2,3,4]) - assert x.get_data() == [1,2,3,4] + assert x.as_list() == [1,2,3,4] assert x.get_names() is None @@ -45,11 +45,11 @@ def test_NamedList_get_slice(): x = NamedList([1,2,3,4]) sub = x.get_slice([0, 2]) - assert sub.get_data() == [1, 3] + assert sub.as_list() == [1, 3] assert sub.get_names() is None sub = x.get_slice([False, True, True, False]) - assert sub.get_data() == [2, 3] + assert sub.as_list() == [2, 3] assert sub.get_names() is None with pytest.raises(Exception) as ex: @@ -58,11 +58,11 @@ def test_NamedList_get_slice(): x.set_names(["a", "b", "c", "d"], in_place=True) sub = x.get_slice([0, 2]) - assert sub.get_data() == [1, 3] + assert sub.as_list() == [1, 3] assert sub.get_names().as_list() == ["a", "c"] sub = x.get_slice(["a", "d"]) - assert sub.get_data() == [1, 4] + assert sub.as_list() == [1, 4] assert sub.get_names().as_list() == ["a", "d"] # with pytest.raises(Exception) as ex: @@ -74,8 +74,8 @@ def test_NamedList_get_item(): x = NamedList([1,2,3,4], names=["a", "b", "c", "d"]) assert x[0] == 1 assert x["b"] == 2 - assert x[[0, 1]].get_data() == [1,2] - assert x[["b","d"]].get_data() == [2,4] + assert x[[0, 1]].as_list() == [1,2] + assert x[["b","d"]].as_list() == [2,4] def test_NamedList_dict(): @@ -83,44 +83,44 @@ def test_NamedList_dict(): assert x.as_dict() == { "a": 1, "b": 2, "c": 3, "d": 4 } x = NamedList.from_dict({ "c": 4, "d": 5, 23: 99 }) - assert x.get_data() == [ 4, 5, 99 ] + assert x.as_list() == [ 4, 5, 99 ] assert x.get_names().as_list() == [ "c", "d", "23" ] def test_NamedList_set_value(): x = NamedList([1,2,3,4]) y = x.set_value(0, 10) - assert y.get_data() == [10, 2, 3, 4] + assert y.as_list() == [10, 2, 3, 4] y = x.set_value(-1, 40) - assert y.get_data() == [1, 2, 3, 40] + assert y.as_list() == [1, 2, 3, 40] y = x.set_value("Aaron", 10) - assert y.get_data() == [1, 2, 3, 4, 10] + assert y.as_list() == [1, 2, 3, 4, 10] assert y.get_names().as_list() == ["", "", "", "", "Aaron"] x.set_names(["a", "b", "c", "d"], in_place=True) y = x.set_value("a", 10) - assert y.get_data() == [10, 2, 3, 4] + assert y.as_list() == [10, 2, 3, 4] y = x.set_value("d", 40) - assert y.get_data() == [1, 2, 3, 40] + assert y.as_list() == [1, 2, 3, 40] y = x.set_value("Aaron", 10) - assert y.get_data() == [1, 2, 3, 4, 10] + assert y.as_list() == [1, 2, 3, 4, 10] assert y.get_names().as_list() == ["a", "b", "c", "d", "Aaron"] def test_NamedList_set_slice(): x = NamedList([1,2,3,4]) y = x.set_slice([0, 3], [10, 40]) - assert y.get_data() == [10, 2, 3, 40] + assert y.as_list() == [10, 2, 3, 40] y = x.set_slice([False, True, True, False], [20, 30]) - assert y.get_data() == [1, 20, 30, 4] + assert y.as_list() == [1, 20, 30, 4] with pytest.raises(IndexError) as ex: x.set_slice(["Aaron"], [10]) assert str(ex.value).find("no names") >= 0 x.set_names(["a", "b", "c", "d"], in_place=True) y = x.set_slice(["a", "d"], [10, 40]) - assert y.get_data() == [10, 2, 3, 40] + assert y.as_list() == [10, 2, 3, 40] # with pytest.raises(KeyError) as ex: # y = x.set_slice(["Aaron"], [10]) # assert str(ex.value).find("Aaron") >= 0 @@ -129,89 +129,89 @@ def test_NamedList_set_slice(): def test_NamedList_setitem(): x = NamedList([1,2,3,4], names=["A", "B", "C", "D"]) x[0] = None - assert x.get_data() == [None, 2, 3, 4] + assert x.as_list() == [None, 2, 3, 4] x["B"] = None - assert x.get_data() == [None, None, 3, 4] + assert x.as_list() == [None, None, 3, 4] x[["C", "D"]] = [30, 40] - assert x.get_data() == [None, None, 30, 40] + assert x.as_list() == [None, None, 30, 40] x["E"] = "FOO" - assert x.get_data() == [None, None, 30, 40, "FOO"] + assert x.as_list() == [None, None, 30, 40, "FOO"] assert x.get_names().as_list() == ["A", "B", "C", "D", "E"] def test_NamedList_insert(): x = NamedList([1,2,3,4]) y = x.safe_insert(2, "FOO") - assert y.get_data() == [1, 2, "FOO", 3, 4] + assert y.as_list() == [1, 2, "FOO", 3, 4] assert y.get_names() is None x.set_names(["A", "B", "C", "D"], in_place=True) x.insert(2, "FOO") - assert x.get_data() == [1, 2, "FOO", 3, 4] + assert x.as_list() == [1, 2, "FOO", 3, 4] assert x.get_names().as_list() == ["A", "B", "", "C", "D"] x.insert("D", None) - assert x.get_data() == [1, 2, "FOO", 3, None, 4] + assert x.as_list() == [1, 2, "FOO", 3, None, 4] assert x.get_names().as_list() == [ "A", "B", "", "C", "", "D"] def test_NamedList_extend(): x = NamedList([1,2,3,4]) y = x.safe_extend([None, 1, True]) - assert y.get_data() == [ 1, 2, 3, 4, None, 1, True ] + assert y.as_list() == [ 1, 2, 3, 4, None, 1, True ] assert y.get_names() is None y = x.safe_extend(NamedList([False, 2, None], names=[ "E", "F", "G" ])) - assert y.get_data() == [ 1, 2, 3, 4, False, 2, None ] + assert y.as_list() == [ 1, 2, 3, 4, False, 2, None ] assert y.get_names().as_list() == [ "", "", "", "", "E", "F", "G" ] x.set_names(["A", "B", "C", "D"], in_place=True) x.extend([None, 1, True]) - assert x.get_data() == [ 1, 2, 3, 4, None, 1, True ] + assert x.as_list() == [ 1, 2, 3, 4, None, 1, True ] assert x.get_names().as_list() == [ "A", "B", "C", "D", "", "", "" ] x.extend(NamedList([False, 2, None], names=[ "E", "F", "G" ])) - assert x.get_data() == [ 1, 2, 3, 4, None, 1, True, False, 2, None ] + assert x.as_list() == [ 1, 2, 3, 4, None, 1, True, False, 2, None ] assert x.get_names().as_list() == [ "A", "B", "C", "D", "", "", "", "E", "F", "G" ] def test_NamedList_append(): x = NamedList([1,2,3,4]) y = x.safe_append(1) - assert y.get_data() == [ 1,2,3,4,1 ] + assert y.as_list() == [ 1,2,3,4,1 ] assert y.get_names() is None x.set_names(["A", "B", "C", "D"], in_place=True) x.append(1) - assert x.get_data() == [ 1,2,3,4,1 ] + assert x.as_list() == [ 1,2,3,4,1 ] assert x.get_names().as_list() == [ "A", "B", "C", "D", "" ] def test_NamedList_addition(): x1 = NamedList([1,2,3,4], names=["A", "B", "C", "D"]) summed = x1 + [5,6,7] - assert summed.get_data() == [1, 2, 3, 4, 5, 6, 7] + assert summed.as_list() == [1, 2, 3, 4, 5, 6, 7] assert summed.get_names().as_list() == [ "A", "B", "C", "D", "", "", "" ] x2 = NamedList([5,6,7], names=["E", "F", "G"]) summed = x1 + x2 - assert summed.get_data() == [1, 2, 3, 4, 5, 6, 7] + assert summed.as_list() == [1, 2, 3, 4, 5, 6, 7] assert summed.get_names().as_list() == ["A", "B", "C", "D", "E", "F", "G"] x1 += x2 - assert x1.get_data() == [1, 2, 3, 4, 5, 6, 7] + assert x1.as_list() == [1, 2, 3, 4, 5, 6, 7] assert x1.get_names().as_list() == ["A", "B", "C", "D", "E", "F", "G"] def test_NamedList_copy(): x = NamedList([1,2,3,4]) y = x.copy() - assert y.get_data() == x.get_data() + assert y.as_list() == x.as_list() assert y.get_names() is None x = NamedList([1,2,3,4], names=["A", "B", "C", "D"]) y = deepcopy(x) - assert y.get_data() == x.get_data() + assert y.as_list() == x.as_list() assert y.get_names() == x.get_names() @@ -219,19 +219,19 @@ def test_NamedList_generics(): x = NamedList([1,2,3,4], names=["A", "B", "C", "D"]) sub = biocutils.subset_sequence(x, [0,3,2,1]) assert isinstance(sub, NamedList) - assert sub.get_data() == [1, 4, 3, 2] + assert sub.as_list() == [1, 4, 3, 2] assert sub.get_names().as_list() == [ "A", "D", "C", "B" ] y = ["a", "b", "c", "d"] com = biocutils.combine_sequences(x, y) assert isinstance(com, NamedList) - assert com.get_data() == [1, 2, 3, 4, "a", "b", "c", "d"] + assert com.as_list() == [1, 2, 3, 4, "a", "b", "c", "d"] assert com.get_names().as_list() == [ "A", "B", "C", "D", "", "", "", "" ] y = biocutils.assign_sequence(x, [1, 3], [ 20, 40 ]) - assert y.get_data() == [ 1, 20, 3, 40 ] + assert y.as_list() == [ 1, 20, 3, 40 ] assert y.get_names().as_list() == [ "A", "B", "C", "D" ] y = biocutils.assign_sequence(x, [1, 3], NamedList([ 20, 40 ], names=["b", "d" ])) - assert y.get_data() == [ 1, 20, 3, 40 ] + assert y.as_list() == [ 1, 20, 3, 40 ] assert y.get_names().as_list() == [ "A", "B", "C", "D" ] # doesn't set the names, as per policy. diff --git a/tests/test_StringList.py b/tests/test_StringList.py index cc9aed2..394ceac 100644 --- a/tests/test_StringList.py +++ b/tests/test_StringList.py @@ -5,24 +5,24 @@ def test_StringList_init(): x = StringList([1,2,3,4]) assert isinstance(x, StringList) - assert x.get_data() == [ '1', '2', '3', '4' ] + assert x.as_list() == [ '1', '2', '3', '4' ] assert x.get_names() is None # Constructor works with other StringList objects. recon = StringList(x) - assert recon.get_data() == x.get_data() + assert recon.as_list() == x.as_list() empty = StringList() - assert empty.get_data() == [] + assert empty.as_list() == [] # Constructor works with Nones. x = StringList([1,None,None,4]) - assert x.get_data() == [ '1', None, None, '4' ] + assert x.as_list() == [ '1', None, None, '4' ] # Constructor works with other NamedList objects. x = NamedList([True, False, None, 2]) recon = StringList(x) - assert recon.get_data() == ["True", "False", None, "2"] + assert recon.as_list() == ["True", "False", None, "2"] def test_StringList_getitem(): @@ -31,35 +31,35 @@ def test_StringList_getitem(): assert x[0] == "1" sub = x[1:3] assert isinstance(sub, StringList) - assert sub.get_data() == ["2", "3"] + assert sub.as_list() == ["2", "3"] x.set_names(["A", "B", "C", "D"], in_place=True) assert x["C"] == "3" sub = x[["C", "D", "A", "B"]] assert isinstance(sub, StringList) - assert sub.get_data() == ["3", "4", "1", "2"] + assert sub.as_list() == ["3", "4", "1", "2"] def test_StringList_setitem(): x = StringList([1,2,3,4]) x[0] = None - assert x.get_data() == [None, "2", "3", "4"] + assert x.as_list() == [None, "2", "3", "4"] x[0] = 12345 - assert x.get_data() == ["12345", "2", "3", "4"] + assert x.as_list() == ["12345", "2", "3", "4"] x[1:3] = [10, 20] - assert x.get_data() == ["12345", "10", "20", "4"] + assert x.as_list() == ["12345", "10", "20", "4"] x[0:4:2] = [None, None] - assert x.get_data() == [None, "10", None, "4"] + assert x.as_list() == [None, "10", None, "4"] x.set_names(["A", "B", "C", "D"], in_place=True) x["C"] = "3" - assert x.get_data() == [None, "10", "3", "4"] + assert x.as_list() == [None, "10", "3", "4"] x[["A", "B"]] = [True, False] - assert x.get_data() == ["True", "False", "3", "4"] + assert x.as_list() == ["True", "False", "3", "4"] x["E"] = 50 - assert x.get_data() == ["True", "False", "3", "4", "50"] + assert x.as_list() == ["True", "False", "3", "4", "50"] assert x.get_names().as_list() == [ "A", "B", "C", "D", "E" ] @@ -68,14 +68,14 @@ def test_StringList_mutations(): x = StringList([1,2,3,4]) x.insert(2, None) x.insert(1, "FOO") - assert x.get_data() == [ "1", "FOO", "2", None, "3", "4" ] + assert x.as_list() == [ "1", "FOO", "2", None, "3", "4" ] # Extension: x.extend([None, 1, True]) - assert x.get_data() == [ "1", "FOO", "2", None, "3", "4", None, "1", "True" ] + assert x.as_list() == [ "1", "FOO", "2", None, "3", "4", None, "1", "True" ] alt = StringList([ "YAY", "BAR", "WHEE" ]) x.extend(alt) - assert x.get_data() == [ "1", "FOO", "2", None, "3", "4", None, "1", "True", "YAY", "BAR", "WHEE" ] + assert x.as_list() == [ "1", "FOO", "2", None, "3", "4", None, "1", "True", "YAY", "BAR", "WHEE" ] # Appending: x.append(1) @@ -88,13 +88,13 @@ def test_StringList_generics(): x = StringList([1,2,3,4]) sub = biocutils.subset_sequence(x, [0,3,2,1]) assert isinstance(sub, StringList) - assert sub.get_data() == ["1", "4", "3", "2"] + assert sub.as_list() == ["1", "4", "3", "2"] y = ["a", "b", "c", "d"] com = biocutils.combine_sequences(x, y) assert isinstance(com, StringList) - assert com.get_data() == ["1", "2", "3", "4", "a", "b", "c", "d"] + assert com.as_list() == ["1", "2", "3", "4", "a", "b", "c", "d"] ass = biocutils.assign_sequence(x, [1,3], ["a", "b"]) assert isinstance(ass, StringList) - assert ass.get_data() == ["1", "a", "3", "b"] + assert ass.as_list() == ["1", "a", "3", "b"]