From e7bc887f600d7daee78785f1e9da962f0fba1cba Mon Sep 17 00:00:00 2001 From: prculley Date: Wed, 22 Nov 2017 08:29:11 -0600 Subject: [PATCH] Fix Deep Relationship filter to avoid recursion crash Fixes #10279 This also changes filter to use shortest paths. --- .../person/_deeprelationshippathbetween.py | 67 +++++++++++++------ 1 file changed, 45 insertions(+), 22 deletions(-) diff --git a/gramps/gen/filters/rules/person/_deeprelationshippathbetween.py b/gramps/gen/filters/rules/person/_deeprelationshippathbetween.py index 5ee885433..3740468c3 100644 --- a/gramps/gen/filters/rules/person/_deeprelationshippathbetween.py +++ b/gramps/gen/filters/rules/person/_deeprelationshippathbetween.py @@ -3,6 +3,7 @@ # # Copyright (C) 2009 Robert Ham # Copyright (C) 2011 Adam Stein +# Copyright (C) 2017 Paul Culley # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -24,6 +25,7 @@ # Standard Python modules # #------------------------------------------------------------------------- +from collections import deque from ....const import GRAMPS_LOCALE as glocale _ = glocale.translation.gettext @@ -95,31 +97,52 @@ def get_person_family_people(db, person, person_handle): return people -def find_deep_relations(db, user, person, path, seen, target_people): - if len(target_people) < 1: - return [] +def find_deep_relations(db, user, person, target_people): + """ This explores all possible paths between a person and one or more + targets. The algorithm processes paths in a breadth first wave, one + remove at a time. The first path that reaches a target causes the target + to be marked complete and the path to be stored in the return_paths set. + By processing in wave, the return path should be a shortest path. + The function stores to do data and intermediate results in an ordered dict, + rather than using a recursive algorithm becasue some trees have been found + that exceed the standard python recursive depth. """ + return_paths = set() # all people in paths between targets and person if person is None: - return [] + return return_paths + todo = deque([person.handle]) # list of work to do, handles, add to right, + # pop from left + done = {} # The key records handles already examined, + # the value is a handle of the previous person in the path, or None at + # head of path. This forms a linked list of handles along the path. + done[person.handle] = None - handle = person.get_handle() - if handle in seen: - return [] - seen.append(handle) - if user: - user.step_progress() + while todo: + handle = todo.popleft() - return_paths = [] - person_path = path + [handle] + if user: + user.step_progress() - if handle in target_people: - return_paths += [person_path] - target_people.remove(handle) + if handle in target_people: # if we found a target + prev_hndl = handle + # Go through linked list and save the handles in return_paths + while prev_hndl: + return_paths.add(prev_hndl) + prev_hndl = done[prev_hndl] + target_people.remove(handle) + if not target_people: # Quit searching if all targets found + break - family_people = get_person_family_people(db, person, handle) - for family_person in family_people: - pers = db.get_person_from_handle(family_person) - return_paths += find_deep_relations(db, user, pers, person_path, seen, target_people) + person = db.get_person_from_handle(handle) + if person is None: + continue + + people = get_person_family_people(db, person, handle) + for p_hndl in people: + if p_hndl in done: # check if we have already been here + continue # and ignore if we have + todo.append(p_hndl) # Add to the todo list + done[p_hndl] = handle # Add to the (almost) done list return return_paths @@ -147,12 +170,12 @@ class DeepRelationshipPathBetween(Rule): user.begin_progress(_('Finding relationship paths'), _('Evaluating people'), db.get_number_of_people()) - paths = find_deep_relations(db, user, root_person, [], [], target_people) + self.__matches = find_deep_relations(db, user, root_person, target_people) if user: user.end_progress() - self.__matches = set() - list(map(self.__matches.update, paths)) +# self.__matches = set() +# list(map(self.__matches.update, paths)) def reset(self): self.__matches = set()