Skip to content
15 changes: 5 additions & 10 deletions src/Model/AutoSegmentation/SegmentationListFilter.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import pandas
import pathlib
import contextlib
from src.Controller.PathHandler import resource_path


Expand Down Expand Up @@ -28,7 +29,7 @@ def _dict_row_filter(data: pandas.DataFrame, csv_column: str = None, filter_valu
:return: pandas.DataFrame
"""

if filter_values is not None and csv_column is not None:
if filter_values and csv_column is not None:
filter_list: dict | list[...] | None = None
if isinstance(filter_values, dict):
filter_list: list[str] = list(filter_values.values())
Expand All @@ -45,18 +46,12 @@ def _column_filter(data: pandas.DataFrame, column_list: list[str]) -> pandas.Dat
:param *args: list[str]: list of column to filter to
:return: pandas.DataFrame
"""

output: pandas.DataFrame = pandas.DataFrame()
if column_list is not None and not data.empty:
output: pandas.DataFrame = pandas.DataFrame()
for column in column_list:
try:
with contextlib.suppress(KeyError):
output[column]: pandas.Series = data[column]
finally:
continue
if output.empty:
return data
return output
return data
return data if output.empty else output

def _strip_whitespace(data: pandas.DataFrame) -> pandas.DataFrame:
"""
Expand Down
2 changes: 1 addition & 1 deletion src/View/AutoSegmentation/AutoSegmentWindow.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def __init__(self, view_state: AutoSegmentViewState) -> None:

# Member Variables
self._view_state = view_state
self._tree_selector: SegmentSelectorWidget = SegmentSelectorWidget(self, view_state.segmentation_list, update_callback=self.update_save_start_button_states)
self._tree_selector: SegmentSelectorWidget = SegmentSelectorWidget(view_state.segmentation_list, update_callback=self.update_save_start_button_states)
self._start_button: QtWidgets.QPushButton = QtWidgets.QPushButton("Start")
self._start_button.setEnabled(False)
self._progress_text: QtWidgets.QTextEdit = QtWidgets.QTextEdit()
Expand Down
17 changes: 11 additions & 6 deletions src/View/AutoSegmentation/ButtonInputBox.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,16 +91,21 @@ def _positive_action(self) -> None:

:returns: None
"""
if self.text is not None:
self.typed_text = self.text.text()
if self.delete_word:
self.typed_text = self.delete_word
return self._send(self.typed_text)
if self._send is not None:
if self.text is not None:
self.typed_text = self.text.text()
if self.delete_word:
self.typed_text = self.delete_word
return self._send(self.typed_text)
return None

def _negative_action(self) -> None:
"""
Action Being Performed if the Cancel(Red) button is clicked

:returns: None
"""
self._close()
if self._close is not None:
self._close()
else:
self.close()
53 changes: 42 additions & 11 deletions src/View/AutoSegmentation/SegmentSelectorWidget.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,26 +25,28 @@ class SegmentSelectorWidget(QtWidgets.QWidget):
which returns a list[str]
"""

def __init__(self, parent, segmentation_list: list[str], data_location="data/csv", update_callback: Callable[[], None] | None = None) -> None:
def __init__(self, segmentation_list: list[str] = None, data_location="data/csv", update_callback: Callable[[], None] | None = None) -> None:
"""
Initialisation of the SegmentSelectorWidget.
Constructs the parent class of QtWidgets.QWidget
Creates the list for the storage of the selected segments
Generates Tree
Sets up the layout of the Widget

:param parent: QWidget
:param segmentation_list: list[str]
:param data_location: str
:returns: None
"""

super(SegmentSelectorWidget, self).__init__(parent)
super(SegmentSelectorWidget, self).__init__()
self.setStyleSheet(StyleSheetReader().get_stylesheet()) # To style the Widget

# Class Members
# Reference to the list owned by another class most likely the controller class
self._selected_list: list[str] = segmentation_list
if segmentation_list is not None:
self._selected_list: list[str] = segmentation_list
else:
self._selected_list: list[str] = []

# References to look up items in tree without searching entire tree
self._tree_choices_ref: dict[str, list[QTreeWidgetItem]] = {}

Expand Down Expand Up @@ -232,7 +234,7 @@ def _parent_states(self, item: QTreeWidgetItem) -> None:
# Removes full body section when the body section unchecked
self._parent_section_changed(item, Qt.CheckState.Unchecked)

def _child_states(self, item: QTreeWidgetItem, body_text) -> None:
def _child_states(self, item: QTreeWidgetItem, body_text: str) -> None:
"""
Determines if check box was checked or unchecked then determines the action it needs to take

Expand Down Expand Up @@ -295,14 +297,30 @@ def _setting_parent_states(self, item: QTreeWidgetItem) -> None:
item.setCheckState(0, Qt.CheckState.PartiallyChecked)


def _selected_list_add_or_remove(self, body_text: str , state: Qt.CheckState) -> None:
def _selected_list_add_or_remove(self, body_text: str , state: Qt.CheckState | int) -> None:
"""
To add or remove an item from the self._selection_list member when a selection has changed

To reduce the reliance of tests on an external module PySide6 the
option of using int as a state input is allowed
If State is int then the values are:
< 0 = Unchecked
0 = Unchecked
1 = PartiallyChecked
2 = Checked
> 2 = Checked


:param body_text: str
:param state: Qt.CheckState
:param state: Qt.CheckState | int
:returns: None
"""
if type(state) == int:
if state > 2:
state = 2
elif state < 0:
state = 0
state = Qt.CheckState(state)
if state == Qt.CheckState.Checked and body_text not in self._selected_list:
self._selected_list.append(body_text.strip())
if state == Qt.CheckState.Unchecked and body_text in self._selected_list:
Expand All @@ -311,19 +329,32 @@ def _selected_list_add_or_remove(self, body_text: str , state: Qt.CheckState) ->
self.update_save_start_button_states()


def _uniform_selection(self, check: Qt.CheckState) -> None:
def _uniform_selection(self, check: Qt.CheckState | int) -> None:
"""
Method to set all check boxes to the same state as well as
adding or removing all values from self.selected List.

:param check: Qt.CheckState
To reduce the reliance of tests on an external module PySide6 the
option of using int as a state input is allowed
If State is int then the values are:
< 1 = Unchecked
1 = Unchecked
2 = Checked
> 2 = Checked

:param check: Qt.CheckState | int
:returns: None
"""
# Instead of just going though self._selected_list and Unchecking/Checking only the values
# which exist with the list we are going though every value and Unchecking/Checking all of
# as a potential method of dealing with potential bugs such as checked boxes which are
# not checked which may or may not exist.

if type(check) == int:
if check > 1:
check = 2
else:
check = 0
check = Qt.CheckState(check)
for key, values in self._tree_choices_ref.items():
for item in values:
item.setCheckState(1, check)
Expand Down
150 changes: 150 additions & 0 deletions test/AutoSegment/test_autosegment_list_filter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import pytest
import pandas as pd
from unittest.mock import patch

from src.Model.AutoSegmentation.SegmentationListFilter import read_csv_to_pandas

@pytest.mark.parametrize(
"csv_data, row_filter_column, row_filter_words, column_list, expected_df, test_id",
[
# Happy path: filter by column, dict filter, and select columns
(
pd.DataFrame({
"A": ["x", "y", "z"],
"B": ["foo", "bar", "baz"],
"C": ["1", "2", "3"]
}),
"A",
{"A": "x"},
["B", "C"],
pd.DataFrame({"B": ["foo"], "C": ["1"]}),
"happy_path_dict_filter"
),
# Happy path: filter by column, list filter, and select columns
(
pd.DataFrame({
"A": ["x", "y", "z"],
"B": ["foo", "bar", "baz"],
"C": ["1", "2", "3"]
}),
"A",
["y", "z"],
["A", "B"],
pd.DataFrame({"A": ["y", "z"], "B": ["bar", "baz"]}),
"happy_path_list_filter"
),
# Edge case: no filter, select all columns
(
pd.DataFrame({
"A": ["x", "y"],
"B": ["foo", "bar"]
}),
None,
None,
None,
pd.DataFrame({"A": ["x", "y"], "B": ["foo", "bar"]}),
"edge_no_filter_all_columns"
),
# Edge case: filter column but filter values is empty list
(
pd.DataFrame({
"A": ["x", "y"],
"B": ["foo", "bar"]
}),
"A",
[],
["A", "B"],
pd.DataFrame({
"A": ["x", "y"],
"B": ["foo", "bar"]
}),
"edge_empty_list_filter"
),
# Edge case: filter column but filter values is None
(
pd.DataFrame({
"A": ["x", "y"],
"B": ["foo", "bar"]
}),
"A",
None,
["A", "B"],
pd.DataFrame({"A": ["x", "y"], "B": ["foo", "bar"]}),
"edge_none_filter"
),
# Edge case: column_list contains non-existent column
(
pd.DataFrame({
"A": ["x", "y"],
"B": ["foo", "bar"]
}),
None,
None,
["A", "C"],
pd.DataFrame({"A": ["x", "y"]}),
"edge_nonexistent_column"
),
# Edge case: all whitespace in string columns
(
pd.DataFrame({
"A": [" x ", " y "],
"B": [" foo ", " bar "]
}),
None,
None,
["A", "B"],
pd.DataFrame({"A": ["x", "y"], "B": ["foo", "bar"]}),
"edge_whitespace"
),
# Edge case: empty DataFrame
(
pd.DataFrame(columns=["A", "B"]),
None,
None,
["A", "B"],
pd.DataFrame(columns=["A", "B"]),
"edge_empty_dataframe"
),
],
ids=[
"happy_path_dict_filter",
"happy_path_list_filter",
"edge_no_filter_all_columns",
"edge_empty_list_filter",
"edge_none_filter",
"edge_nonexistent_column",
"edge_whitespace",
"edge_empty_dataframe"
]
)
def test_read_csv_to_pandas_various_cases(csv_data, row_filter_column, row_filter_words, column_list, expected_df, test_id):
# Arrange
with patch("src.Model.AutoSegmentation.SegmentationListFilter.resource_path", side_effect=lambda x: x), \
patch("src.Model.AutoSegmentation.SegmentationListFilter._read_csv_data_to_pandas", return_value=csv_data):

# Act
result = read_csv_to_pandas("fake.csv", row_filter_column, row_filter_words, column_list)

# Assert
pd.testing.assert_frame_equal(result.reset_index(drop=True), expected_df.reset_index(drop=True))

def test_read_csv_to_pandas_file_not_found():
# Arrange
with patch("src.Model.AutoSegmentation.SegmentationListFilter.resource_path", side_effect=lambda x: x), \
patch("src.Model.AutoSegmentation.SegmentationListFilter._read_csv_data_to_pandas", side_effect=FileNotFoundError("not found")):

# Act & Assert
with pytest.raises(FileNotFoundError):
read_csv_to_pandas("notfound.csv")

def test_read_csv_to_pandas_column_list_none_and_empty():
# Arrange
df = pd.DataFrame({"A": [1, 2], "B": [3, 4]})
with patch("src.Model.AutoSegmentation.SegmentationListFilter.resource_path", side_effect=lambda x: x), \
patch("src.Model.AutoSegmentation.SegmentationListFilter._read_csv_data_to_pandas", return_value=df):

# Act
result = read_csv_to_pandas("fake.csv", column_list=None)

# Assert
pd.testing.assert_frame_equal(result, df)
Loading
Loading