Skip to content

Commit

Permalink
Reorganized NamedList code for consistency with Names.
Browse files Browse the repository at this point in the history
- Default copy() is now a little bit deeper, so that it has
  similar behavior to that of a list's copy method.
- Added a __copy__ method, because that wasn't there before.
- Renamed get_data() to as_list(), for consistency with Names.
  Also removed the set_data() method - just make a new object.
  • Loading branch information
LTLA committed Nov 15, 2023
1 parent eecae91 commit 8b7ace8
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 115 deletions.
117 changes: 71 additions & 46 deletions src/biocutils/NamedList.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
"""
Expand All @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand All @@ -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":
"""
Expand Down Expand Up @@ -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":
"""
Expand All @@ -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:
Expand All @@ -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()))


Expand All @@ -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
Expand Down
14 changes: 7 additions & 7 deletions tests/test_Factor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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"])
Expand Down
Loading

0 comments on commit 8b7ace8

Please sign in to comment.