diff --git a/CHANGELOG.md b/CHANGELOG.md index 0551ca15..2cdea6f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added more detailed instructions for installing analysisUtils (#340). - Included initial documentation (#348). - Add badges to README.md (#358). +- Add tests for utilsLines, and tidy up formatting (#363). ### Changed diff --git a/phangsPipeline/utilsLists.py b/phangsPipeline/utilsLists.py index 34c93718..6b4219a4 100644 --- a/phangsPipeline/utilsLists.py +++ b/phangsPipeline/utilsLists.py @@ -1,23 +1,39 @@ -""" -General purpose utilities. -""" - - def select_from_list( - master_list, - first=None, - last=None, - skip=[], - only=[], - loose=True, - ): + master_list: list, + first: str | None = None, + last: str | None = None, + skip: str | list | None = None, + only: str | list | None = None, + loose: bool = True, +) -> list: """ Select only part of a list. + + Args: + master_list (list): List of elements + first (str): First element to select in list. Defaults to None, + which will run from start of list + last (str): Last element to select in list. Defaults to None, + which will run to end of list + skip (str, list): List of elements to skip. Defaults + to None, which will not filter + only (str, list): List of only elements to include. Defaults + to None, which will not filter + loose (bool): If False, for skip/only, will match on case. + Otherwise, will be case agnostic. Defaults to True + + Returns: + list: Sorted list of filtered elements """ - sorted_list = sorted(master_list,key=lambda s: s.lower()) + if not isinstance(master_list, list): + raise TypeError("master_list must be a list") - sub_list = [] + # Start by sorting the list + sorted_list = sorted( + master_list, + key=lambda s: s.lower(), + ) if first is not None: before_first = True @@ -29,118 +45,129 @@ def select_from_list( else: after_last = False + if skip is None: + skip = [] + if isinstance(skip, str): + skip = [skip] + + if only is None: + only = [] + if isinstance(only, str): + only = [only] + + if loose: + skip = [s.lower() for s in skip] + only = [s.lower() for s in only] + + sub_list = [] + for element in sorted_list: + # Check if we're before or after first/last element if first is not None: - if loose: - if element.lower() >= first.lower(): - before_first = False - else: - if element.lower() == first.lower(): - before_first = False + if element.lower() >= first.lower(): + before_first = False if last is not None: - if loose: - if element.lower() > last.lower(): - after_last = True + if element.lower() > last.lower(): + after_last = True - if before_first: + # If we're before first or after last, skip + if before_first or after_last: continue - if after_last: - continue + # Check skip + if len(skip) > 0: + match = False - if skip is not None: - if len(skip) > 0: - match = False - if loose: - for this_skip in skip: - if element.lower() == this_skip.lower(): - match = True - else: - if element in skip: - match = True - if match: - continue - - if only is not None: - if len(only) > 0: - match = False - if loose: - for this_only in only: - if element.lower() == this_only.lower(): - match = True - else: - if element in only: - match = True - if not match: - continue - - sub_list.append(element) + # If we're loose, we've already lowercase-d skip + if loose: + if element.lower() in skip: + match = True - if last is not None: - if not loose: - if element.lower() == last.lower(): - after_last = True + # Else, match exactly + else: + if element in skip: + match = True - return(sub_list) - -def lohi_vecs_to_pairs(lovec, hivec): - """ - Convert a low and hi - """ + # If we've found a match, skip + if match: + continue + + # Check only + if len(only) > 0: + match = False + + # If we're loose, we've already lowercase-d only + if loose: + if element.lower() in only: + match = True + + # Else, match exactly + else: + if element in only: + match = True - # Add some error checking + # If we haven't found a match, skip + if not match: + continue - pairs = [] + sub_list.append(element) - for ii in range(lovec): - pairs.append((lovec[ii], hivec[ii])) + return sub_list - return(pairs) -def merge_pairs(pairs): +def merge_pairs( + pairs: list, +) -> list: """ - Accepted a matched list lo/hi pairs and returns the list of pairs + Accept a matched list lo/hi pairs and returns the list of pairs merged until convergence. + + Args: + pairs (list): list of pairs + + Returns: + list: list of pairs, as tuples """ - - # Add some error checking + + if not isinstance(pairs, list): + raise TypeError("pairs must be a list") # Sort on the x coordinate pairs = sorted(pairs) # Start the list of new pairs - new_pairs = [pairs[0]] + new_pairs = [tuple(pairs[0])] i = 1 # Iterate - while i <= len(pairs)-1: + while i <= len(pairs) - 1: # Pick the last new pair for comparison - x1,y1 = new_pairs[-1] + x1, y1 = new_pairs[-1] # Compare to the current pair - x2,y2 = pairs[i] + x2, y2 = pairs[i] # Since we are sorted by x, we know x2>=x1. If y2 is less than # y1, the current window already spans the range. if y2 <= y1: - i += 1 # included + i += 1 # included continue # If x2 is less than y1, the comparison window begins inside # the end point, then we might extend the window. Span to the # maximum of y1, y2. if x2 <= y1: - new_pairs[-1] = (x1,max(y1,y2)) # grow + new_pairs[-1] = (x1, max(y1, y2)) # grow continue else: # Otherwise, the new window begins outside the previous # window. In that case, we move to a new part of the # sequence. Now use that pair for comparison instead. - new_pairs.append((x2,y2)) # new + new_pairs.append((x2, y2)) # new i += 1 continue - return(new_pairs) + return new_pairs diff --git a/tests/test_utilsLists.py b/tests/test_utilsLists.py new file mode 100644 index 00000000..a77544e7 --- /dev/null +++ b/tests/test_utilsLists.py @@ -0,0 +1,321 @@ +import pytest + +from phangsPipeline.utilsLists import select_from_list, merge_pairs + +class TestSelectFromList: + """Suite of tests for utilsLists.select_from_list""" + + test_list = [ + "ngc0000", + "ngc0123", + "ngc2345", + "ngc3456", + "ngc4567", + ] + + @pytest.mark.xfail(raises=TypeError, reason="Not a list") + def test_pass_non_list(self): + """Test passing a non-list, which should fail""" + + test_list = "not a list" + + select_from_list( + test_list, + ) + + + def test_default_selection(self): + """Test the default selection""" + + filtered_test_list = select_from_list( + self.test_list, + ) + + result = self.test_list + + assert filtered_test_list == result + + def test_first_selection(self): + """Test passing a first element""" + + first = "ngc0123" + + filtered_test_list = select_from_list( + self.test_list, + first=first, + ) + + result = [ + "ngc0123", + "ngc2345", + "ngc3456", + "ngc4567", + ] + + assert filtered_test_list == result + + def test_last_selection(self): + """Test passing a last element""" + + last = "ngc3456" + + filtered_test_list = select_from_list( + self.test_list, + last=last, + ) + + result = [ + "ngc0000", + "ngc0123", + "ngc2345", + "ngc3456", + ] + + assert filtered_test_list == result + + def test_first_last_selection(self): + """Test passing a first element and last element""" + + first = "ngc0123" + last = "ngc3456" + + filtered_test_list = select_from_list( + self.test_list, + first=first, + last=last, + ) + + result = [ + "ngc0123", + "ngc2345", + "ngc3456", + ] + + assert filtered_test_list == result + + def test_single_skip(self): + """Test passing a single skip element""" + + skip = "ngc0123" + + filtered_test_list = select_from_list( + self.test_list, + skip=skip, + ) + + result = [ + "ngc0000", + "ngc2345", + "ngc3456", + "ngc4567", + ] + + assert filtered_test_list == result + + def test_multiple_skip(self): + """Test passing multiple skip elements""" + + skip = [ + "ngc0123", + "ngc2345", + ] + + filtered_test_list = select_from_list( + self.test_list, + skip=skip, + ) + + result = [ + "ngc0000", + "ngc3456", + "ngc4567", + ] + + assert filtered_test_list == result + + def test_single_only(self): + """Test passing a single only element""" + + only = "ngc0123" + + filtered_test_list = select_from_list( + self.test_list, + only=only, + ) + + result = [ + "ngc0123" + ] + + assert filtered_test_list == result + + def test_multiple_only(self): + """Test passing multiple only elements""" + + only = [ + "ngc0123", + "ngc2345", + ] + + filtered_test_list = select_from_list( + self.test_list, + only=only, + ) + + result = [ + "ngc0123", + "ngc2345", + ] + + assert filtered_test_list == result + + def test_loose_skip(self): + """Test loose works as expected for skips""" + + skip = ["NGC0123"] + + filtered_test_list = select_from_list( + self.test_list, + skip=skip, + loose=False, + ) + + result = self.test_list + + assert filtered_test_list == result + + def test_loose_lowercase_skip(self): + """Test loose works as expected for lowercase skips""" + + skip = ["ngc0123"] + + filtered_test_list = select_from_list( + self.test_list, + skip=skip, + loose=False, + ) + + result = [ + "ngc0000", + "ngc2345", + "ngc3456", + "ngc4567", + ] + + assert filtered_test_list == result + + def test_loose_only(self): + """Test loose works as expected for only""" + + only = ["NGC0123"] + + result = [] + + filtered_test_list = select_from_list( + self.test_list, + only=only, + loose=False, + ) + + assert filtered_test_list == result + + def test_loose_lowercase_only(self): + """Test loose works as expected for lowercase only""" + + only = ["ngc0123"] + + filtered_test_list = select_from_list( + self.test_list, + only=only, + loose=False, + ) + + result = [ + "ngc0123" + ] + + assert filtered_test_list == result + + +class TestMergePairs: + """Suite of tests for utilsLists.merge_pairs""" + + @pytest.mark.xfail(raises=TypeError, reason="Not a list") + def test_pass_non_list(self): + """Test passing a non-list, which should fail""" + + test_pairs = "not a list" + + merge_pairs(test_pairs) + + def test_sorted_merge(self): + """Test values are merged for an already sorted list""" + + test_pairs = [ + [0, 1], + [2, 3], + [1, 2], + ] + + result = [ + (0, 3), + ] + + merged_pairs = merge_pairs(test_pairs) + + assert merged_pairs == result + + def test_unsorted_merge(self): + """Test values are merged for an unsorted list""" + + test_pairs = [ + [0, 1], + [1, 2], + [2, 3], + ] + + result = [ + (0, 3), + ] + + merged_pairs = merge_pairs(test_pairs) + + assert merged_pairs == result + + def test_sorted_non_merge(self): + """Test values are not merged for an already sorted list""" + + test_pairs = [ + [0, 1], + [2, 3], + [4, 5], + ] + + result = [ + (0, 1), + (2, 3), + (4, 5), + ] + + merged_pairs = merge_pairs(test_pairs) + + assert merged_pairs == result + + def test_unsorted_non_merge(self): + """Test values are not merged for an unsorted list""" + + test_pairs = [ + [2, 3], + [0, 1], + [4, 5], + ] + + result = [ + (0, 1), + (2, 3), + (4, 5), + ] + + merged_pairs = merge_pairs(test_pairs) + + assert merged_pairs == result