diff --git a/core/app.py b/core/app.py index b2a7ad51..481ec40e 100644 --- a/core/app.py +++ b/core/app.py @@ -98,12 +98,17 @@ class DupeGuru(RegistrableApplication, Broadcaster): except EnvironmentError: return None + def _results_changed(self): + self.selected_dupes = [d for d in self.selected_dupes + if self.results.get_group_of_duplicate(d) is not None] + self.notify('results_changed') + def _job_completed(self, jobid): # Must be called by subclasses when they detect that an async job is completed. if jobid == JOB_SCAN: - self.notify('results_changed') + self._results_changed() elif jobid in (JOB_LOAD, JOB_MOVE, JOB_DELETE): - self.notify('results_changed') + self._results_changed() self.notify('problems_changed') @staticmethod @@ -171,7 +176,7 @@ class DupeGuru(RegistrableApplication, Broadcaster): filter = escape(filter, set('()[]\\.|+?^')) filter = escape(filter, '*', '.') self.results.apply_filter(filter) - self.notify('results_changed') + self._results_changed() def clean_empty_dirs(self, path): if self.options['clean_empty_dirs']: @@ -316,7 +321,7 @@ class DupeGuru(RegistrableApplication, Broadcaster): def remove_marked(self): self.results.perform_on_marked(lambda x:None, True) - self.notify('results_changed') + self._results_changed() def remove_selected(self): self.remove_duplicates(self.selected_dupes) @@ -356,7 +361,7 @@ class DupeGuru(RegistrableApplication, Broadcaster): if not self.directories.has_any_file(): raise NoScannableFileError() self.results.groups = [] - self.notify('results_changed') + self._results_changed() self._start_job(JOB_SCAN, do) def toggle_selected_mark_state(self): diff --git a/core/results.py b/core/results.py index b4c1ea01..a0e871b0 100644 --- a/core/results.py +++ b/core/results.py @@ -259,11 +259,14 @@ class Results(Markable): group = self.get_group_of_duplicate(dupe) if dupe not in group.dupes: return + ref = group.ref group.remove_dupe(dupe, False) + del self.__group_of_duplicate[dupe] self._remove_mark_flag(dupe) self.__total_count -= 1 self.__total_size -= dupe.size if not group: + del self.__group_of_duplicate[ref] self.__groups.remove(group) if self.__filtered_groups: self.__filtered_groups.remove(group) diff --git a/core/tests/app_test.py b/core/tests/app_test.py index 096db89f..1de54008 100644 --- a/core/tests/app_test.py +++ b/core/tests/app_test.py @@ -390,6 +390,16 @@ class TestCaseDupeGuruWithResults: add_fake_files_to_directories(app.directories, self.objects) # We want the scan to at least start app.start_scanning() # will be cancelled immediately eq_(len(self.rtable), 0) + + def test_selected_dupes_after_removal(self, do_setup): + # Purge the app's `selected_dupes` attribute when removing dupes, or else it might cause a + # crash later with None refs. + app = self.app + app.results.mark_all() + self.rtable.select([0, 1, 2, 3, 4]) + app.remove_marked() + eq_(len(self.rtable), 0) + eq_(app.selected_dupes, []) class TestCaseDupeGuru_renameSelected: def pytest_funcarg__do_setup(self, request): diff --git a/core/tests/results_test.py b/core/tests/results_test.py index 6da07880..86383ae6 100644 --- a/core/tests/results_test.py +++ b/core/tests/results_test.py @@ -231,6 +231,15 @@ class TestCaseResultsWithSomeGroups: self.results.perform_on_marked(lambda x:None, True) assert not self.results.is_modified + def test_group_of_duplicate_after_removal(self): + # removing a duplicate also removes it from the dupe:group map. + dupe = self.results.groups[1].dupes[0] + ref = self.results.groups[1].ref + self.results.remove_duplicates([dupe]) + assert self.results.get_group_of_duplicate(dupe) is None + # also remove group ref + assert self.results.get_group_of_duplicate(ref) is None + class TestCaseResultsWithSavedResults: def setup_method(self, method):