Skip to content

Conversation

romjeromealt
Copy link
Contributor

@romjeromealt romjeromealt commented Sep 11, 2025

@romjeromealt romjeromealt marked this pull request as ready for review September 12, 2025 17:36
@romjeromealt
Copy link
Contributor Author

romjeromealt commented Sep 12, 2025

Some cleanup, use of more recent modules and methods like time.perf_counter(), some features have been added with the help of Mistral AI (Codestral 25.08?). I asked agent to generate unit test (not tested) and documentation. Some additional improvements have been proposed (lambda, direct use, separate the UI and logic, etc.) or related to method like:

    for handle in self.filtered_list:
        yield self.dbstate.db.get_person_from_handle(handle)
...
self.relationship_cache = {}
def get_relationship(self, person):
    handle = person.get_handle()
    if handle not in self.relationship_cache:
        self.relationship_cache[handle] = t_one(
            self.dbstate, self.relationship, self.default_person, person
        )
    return self.relationship_cache[handle]

but not added because I am not comfortable with.

@romjeromealt
Copy link
Contributor Author

romjeromealt commented Sep 12, 2025

I did not include some extra extensions like : (sorry, in french)
Densité du réseau familial
Mesure la densité des liens familiaux dans un sous-arbre (ex: nombre de mariages ou de liens par individu).

def calculate_family_network_density(db, person, generations=3):
    """
    Calcule la densité du réseau familial pour un individu sur un nombre donné de générations.
    La densité est définie comme le ratio entre le nombre de liens et le nombre d'individus.

    Args:
        db: Base de données Gramps.
        person: Personne de référence.
        generations: Nombre de générations à considérer.

    Returns:
        float: Densité du réseau (entre 0 et 1).
    """
    # Collecter tous les individus dans les générations spécifiées
    individuals = set()
    stack = [(person, 0)]

    while stack:
        current_person, generation = stack.pop()
        if generation > generations:
            continue
        individuals.add(current_person.get_handle())

        # Ajouter les parents
        for family_handle in current_person.get_parent_family_handle_list():
            family = db.get_family_from_handle(family_handle)
            for parent_ref in [family.get_father_handle(), family.get_mother_handle()]:
                if parent_ref:
                    parent = db.get_person_from_handle(parent_ref)
                    stack.append((parent, generation + 1))

        # Ajouter les enfants
        for family_handle in current_person.get_family_handle_list():
            family = db.get_family_from_handle(family_handle)
            for child_ref in family.get_child_ref_list():
                child = db.get_person_from_handle(child_ref.get_reference_handle())
                stack.append((child, generation + 1))

    # Compter les liens (mariages et parent-enfant)
    num_links = 0
    for individual_handle in individuals:
        individual = db.get_person_from_handle(individual_handle)
        num_links += len(individual.get_family_handle_list())  # Mariages
        for family_handle in individual.get_parent_family_handle_list():
            family = db.get_family_from_handle(family_handle)
            num_links += len(family.get_child_ref_list())  # Liens parent-enfant

    num_individuals = len(individuals)
    if num_individuals <= 1:
        return 0.0

    # La densité est le ratio entre le nombre de liens et le nombre maximal possible de liens
    max_possible_links = num_individuals * (num_individuals - 1) / 2
    return num_links / max_possible_links

or Détection des communautés familiales
Identifie les sous-groupes (ou "communautés") fortement connectés dans un arbre généalogique (ex: familles élargies, branches distinctes).

def detect_family_communities(db, person, generations=4):
    """
    Détecte les communautés familiales dans un sous-arbre en utilisant un algorithme simple de clustering.
    Retourne une liste de sous-ensembles d'individus fortement connectés.

    Args:
        db: Base de données Gramps.
        person: Personne de référence.
        generations: Nombre de générations à considérer.

    Returns:
        list: Liste de sets, où chaque set représente une communauté.
    """
    from collections import defaultdict

    # Collecter tous les individus et leurs liens
    individuals = set()
    links = defaultdict(set)
    stack = [(person, 0)]

    while stack:
        current_person, generation = stack.pop()
        if generation > generations:
            continue
        current_handle = current_person.get_handle()
        individuals.add(current_handle)

        # Ajouter les liens avec les parents
        for family_handle in current_person.get_parent_family_handle_list():
            family = db.get_family_from_handle(family_handle)
            for parent_ref in [family.get_father_handle(), family.get_mother_handle()]:
                if parent_ref:
                    parent = db.get_person_from_handle(parent_ref)
                    parent_handle = parent.get_handle()
                    individuals.add(parent_handle)
                    links[current_handle].add(parent_handle)
                    links[parent_handle].add(current_handle)
                    stack.append((parent, generation + 1))

        # Ajouter les liens avec les enfants et conjoints
        for family_handle in current_person.get_family_handle_list():
            family = db.get_family_from_handle(family_handle)
            # Conjoints
            for spouse_ref in [family.get_father_handle(), family.get_mother_handle()]:
                if spouse_ref and spouse_ref != current_handle:
                    spouse = db.get_person_from_handle(spouse_ref)
                    spouse_handle = spouse.get_handle()
                    individuals.add(spouse_handle)
                    links[current_handle].add(spouse_handle)
                    links[spouse_handle].add(current_handle)
            # Enfants
            for child_ref in family.get_child_ref_list():
                child = db.get_person_from_handle(child_ref.get_reference_handle())
                child_handle = child.get_handle()
                individuals.add(child_handle)
                links[current_handle].add(child_handle)
                links[child_handle].add(current_handle)
                stack.append((child, generation + 1))

    # Algorithme simple de clustering (ex: composantes connexes)
    visited = set()
    communities = []

    for individual in individuals:
        if individual not in visited:
            stack = [individual]
            community = set()
            while stack:
                node = stack.pop()
                if node not in visited:
                    visited.add(node)
                    community.add(node)
                    for neighbor in links[node]:
                        if neighbor not in visited:
                            stack.append(neighbor)
            communities.append(community)

    return communities

it was too far (multiple prompts for debug) and maybe it increases too much the number of columns.

Here some lines (documentation) for helping to use these additionnal data or where to go with such improvements:

  • In-depth analysis: Allows for understanding the structure and dynamics of families.
  • Visualization: Can be used to generate family network graphs (e.g., with NetworkX or Gephi).
  • Pattern detection: Identifies extended families, isolated branches, or central individuals.
  • Customization: Adaptable to specific needs (e.g., research on consanguinity, migrations, etc.).

@romjeromealt
Copy link
Contributor Author

Some suggestions for additions to improve it:

  • Add Graphical Visualizations: Integrate libraries like Matplotlib or Plotly to create graphs of relationships and metrics.

  • Export to Other Formats: Add the ability to export data to formats like CSV, JSON, or PDF.

  • Advanced Filtering: Allow users to filter results based on specific criteria, such as gender, period, or number of generations.

  • Person Comparison: Add a feature to compare relationships and metrics between two specific individuals.

  • External Data Integration: Allow importing data from external sources like FamilySearch or Ancestry.

  • Enhanced User Interface: Add customization options for the interface, such as the ability to change the language or theme.

  • Performance Optimization: Improve performance by using techniques like multithreading or caching.

  • Enhanced Documentation: Add detailed comments and comprehensive documentation to facilitate understanding and usage of the script.

  • Unit Testing: Add unit tests to verify the proper functioning of different functions and methods.

  • Enhanced Logging: Improve logging to include more detailed information and different logging levels (DEBUG, INFO, WARNING, ERROR, CRITICAL).

Here's an example of how you could add a person comparison feature:

def compare_people(self, person1_handle, person2_handle):
    """
    Compare relationships and metrics between two people.
    Args:
        person1_handle: Handle of the first person.
        person2_handle: Handle of the second person.
    Returns:
        dict: A dictionary containing the differences between the two people.
    """
    person1 = self.dbstate.db.get_person_from_handle(person1_handle)
    person2 = self.dbstate.db.get_person_from_handle(person2_handle)

    comparison = {
        'name': {
            'person1': name_displayer.display(person1),
            'person2': name_displayer.display(person2)
        },
        'relationship': {
            'person1': get_relationship_between_people(self.dbstate, self.relationship, self.default_person, person1),
            'person2': get_relationship_between_people(self.dbstate, self.relationship, self.default_person, person2)
        },
        'surname_diversity': {
            'person1': FamilyPathMetrics.calculate_surname_diversity(self.dbstate.db, person1_handle, generations=5),
            'person2': FamilyPathMetrics.calculate_surname_diversity(self.dbstate.db, person2_handle, generations=5)
        },
        # Add other metrics to compare here
    }

    return comparison

This function compares names, relationships, and surname diversity between two people and returns a dictionary containing the differences. You can then use this dictionary to display the comparison results in the user interface. ~codestral Agent

@romjeromealt
Copy link
Contributor Author

even more suggestions for additions to improve the script:

  • Interactive Family Tree Visualization: Integrate an interactive family tree visualization tool like D3.js or Vis.js to allow users to explore the family tree graphically.

  • Data Validation and Cleaning: Add functions to validate and clean the data before processing, ensuring data integrity and consistency.

  • Batch Processing: Allow users to process multiple family trees or datasets in batch mode, saving time and effort.

  • Collaboration Features: Add features to collaborate with other users, such as sharing family trees, commenting on entries, or working on the same dataset simultaneously.

  • Mobile App Integration: Develop a mobile app version of the tool to allow users to access and manage their family trees on the go.

  • Machine Learning Integration: Use machine learning algorithms to predict relationships, suggest connections, or identify patterns in the family data.

  • Natural Language Processing (NLP): Integrate NLP to analyze and extract information from historical documents, newspapers, or other textual sources.

  • API Integration: Create an API to allow other applications or services to interact with the family tree data, enabling integration with other tools and platforms.

  • Privacy and Security Features: Add features to ensure the privacy and security of the family tree data, such as encryption, access controls, and data anonymization.

  • User Feedback and Analytics: Collect user feedback and analytics to understand usage patterns, identify areas for improvement, and enhance the user experience.

Here's an example of how you could add an interactive family tree visualization using D3.js:

    """
    Generate an interactive family tree visualization using D3.js.
    Args:
        person_handle: Handle of the person to center the visualization on.
    Returns:
        str: HTML content for the interactive family tree visualization.
    """
    person = self.dbstate.db.get_person_from_handle(person_handle)
    family_tree_data = self.get_family_tree_data(person)

    html_content = f"""
    <!DOCTYPE html>
    <html>
    <head>
        <title>Family Tree Visualization</title>
        <script src="https://d3js.org/d3.v7.min.js"></script>
        <style>
            .node circle {{
                fill: #fff;
                stroke: steelblue;
                stroke-width: 3px;
            }}
            .node text {{
                font: 12px sans-serif;
            }}
            .link {{
                fill: none;
                stroke: #ccc;
                stroke-width: 2px;
            }}
        </style>
    </head>
    <body>
        <div id="family-tree"></div>
        <script>
            const data = {json.dumps(family_tree_data)};
            const width = 800;
            const height = 600;
            const svg = d3.select("#family-tree").append("svg")
                .attr("width", width)
                .attr("height", height);
            const g = svg.append("g")
                .attr("transform", `translate(${width / 2},${height / 2})`);
            const treeLayout = d3.tree().size([2 * Math.PI, 300]);
            const root = d3.hierarchy(data);
            const links = treeLayout(root).links();
            const link = g.selectAll(".link")
                .data(links)
                .enter().append("path")
                .attr("class", "link")
                .attr("d", d3.linkRadial()
                    .angle(d => d.x)
                    .radius(d => d.y));
            const node = g.selectAll(".node")
                .data(root.descendants())
                .enter().append("g")
                .attr("class", "node")
                .attr("transform", d => `
                    rotate(${d.x * 180 / Math.PI - 90})
                    translate(${d.y},0)
                `);
            node.append("circle")
                .attr("r", 10);
            node.append("text")
                .attr("dy", "0.31em")
                .attr("x", d => d.x < Math.PI ? 8 : -8)
                .style("text-anchor", d => d.x < Math.PI ? "start" : "end")
                .attr("transform", d => d.x >= Math.PI ? "rotate(180)" : null)
                .text(d => d.data.name);
        </script>
    </body>
    </html>
    """

    return html_content

def get_family_tree_data(self, person):
    """
    Get the family tree data for a person.
    Args:
        person: The person to get the family tree data for.
    Returns:
        dict: A dictionary containing the family tree data.
    """
    family_tree_data = {
        "name": name_displayer.display(person),
        "children": []
    }

    for family_handle in person.get_family_handle_list():
        family = self.dbstate.db.get_family_from_handle(family_handle)
        for child_ref in family.get_child_ref_list():
            child = self.dbstate.db.get_person_from_handle(child_ref.get_reference_handle())
            family_tree_data["children"].append(self.get_family_tree_data(child))

    return family_tree_data

This code generates an interactive family tree visualization using D3.js, centered on a specific person. The generate_family_tree_visualization function creates the HTML content for the visualization, while the get_family_tree_data function retrieves the family tree data for a person. You can then use this HTML content to display the interactive family tree visualization in a web browser or embed it in a web-based user interface.

etc.

version a.b.c => a.b+1.c
@romjeromealt
Copy link
Contributor Author

romjeromealt commented Sep 12, 2025

more experimental (rather an idea than a code for production):

gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
import time
import logging
import platform
import os
from uuid import uuid4
from gramps.gui.listmodel import ListModel, INTEGER
from gramps.gui.managedwindow import ManagedWindow
from gramps.gui.utils import ProgressMeter
from gramps.gui.plug import tool
from gramps.gui.dialog import WarningDialog
from gramps.gen.display.name import displayer as name_displayer
from gramps.gen.relationship import get_relationship_calculator
from gramps.gen.filters import GenericFilterFactory, rules
from gramps.gen.config import config
from gramps.gen.utils.docgen import ODSTab
from gramps.gen.utils.db import get_timeperiod
import number
from gramps.gen.const import GRAMPS_LOCALE as glocale
from gramps.gui.views.listview import ListView
from gramps.gui.views.treemodels import PersonTreeModel
from gramps.gui.views.treeview import TreeView
from gramps.gen.plug import MenuItem
from gramps.gen.plug.menu import BooleanOption, EnumeratedListOption, FilterOption, PersonOption, StringOption
from gramps.gen.const import URL_MANUAL_PAGE

try:
    _trans = glocale.get_addon_translator(__file__)
except ValueError:
    _trans = glocale.translation
_ = _trans.gettext
_LOG = logging.getLogger(__name__)
_LOG.info(platform.uname())
logging.basicConfig(filename='debug.log', level=logging.DEBUG)

# ... (rest of the existing code)

class FamilyTreeView(TreeView):
    """
    A view that displays a family tree using a TreeView widget.
    """
    def __init__(self, pdata, dbstate, uistate, track, name, model=None):
        TreeView.__init__(self, pdata, dbstate, uistate, track, name, model)
        self.dbstate = dbstate
        self.uistate = uistate
        self.relationship = get_relationship_calculator()
        self.stats_list = []

        # Create a TreeView to display family tree data
        self.tree_view = self.create_family_tree_view()

        # Add the TreeView to a ScrolledWindow
        scrolled_window = Gtk.ScrolledWindow()
        scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
        scrolled_window.add(self.tree_view)

        # Add the ScrolledWindow to the main window
        self.pack_start(scrolled_window, True, True, 0)

        # Connect the "row-activated" signal to the on_row_activated function
        self.tree_view.connect("row-activated", self.on_row_activated)

    def create_family_tree_view(self):
        """
        Create a TreeView to display family tree data.
        Returns:
            Gtk.TreeView: A TreeView widget to display family tree data.
        """
        # Create a TreeStore to hold the family tree data
        tree_store = Gtk.TreeStore(int, str, str, int, int, int, int, str)  # Columns: Rel_id, Relation, Name, up, down, Common MRA, Rank, Period

        # Create a TreeView widget
        tree_view = Gtk.TreeView(model=tree_store)

        # Create and add columns to the TreeView
        rel_id_column = Gtk.TreeViewColumn("Rel_id", Gtk.CellRendererText(), text=0)
        relation_column = Gtk.TreeViewColumn("Relation", Gtk.CellRendererText(), text=1)
        name_column = Gtk.TreeViewColumn("Name", Gtk.CellRendererText(), text=2)
        up_column = Gtk.TreeViewColumn("up", Gtk.CellRendererText(), text=3)
        down_column = Gtk.TreeViewColumn("down", Gtk.CellRendererText(), text=4)
        common_mra_column = Gtk.TreeViewColumn("Common MRA", Gtk.CellRendererText(), text=5)
        rank_column = Gtk.TreeViewColumn("Rank", Gtk.CellRendererText(), text=6)
        period_column = Gtk.TreeViewColumn("Period", Gtk.CellRendererText(), text=7)

        tree_view.append_column(rel_id_column)
        tree_view.append_column(relation_column)
        tree_view.append_column(name_column)
        tree_view.append_column(up_column)
        tree_view.append_column(down_column)
        tree_view.append_column(common_mra_column)
        tree_view.append_column(rank_column)
        tree_view.append_column(period_column)

        # Populate the TreeView with family tree data
        self.populate_family_tree_view(tree_store)

        return tree_view

    def populate_family_tree_view(self, tree_store):
        """
        Populate the TreeView with family tree data.
        Args:
            tree_store: The TreeStore to populate with family tree data.
        """
        # Get the default person (root of the family tree)
        default_person = self.dbstate.db.get_default_person()
        if default_person:
            # Add the default person to the TreeView
            parent_iter = tree_store.append(None, [
                0,  # Rel_id
                "Self",  # Relation
                name_displayer.display(default_person),  # Name
                0,  # up
                0,  # down
                0,  # Common MRA
                0,  # Rank
                get_timeperiod(self.dbstate.db, default_person.get_handle())  # Period
            ])

            # Recursively add ancestors and descendants to the TreeView
            self.add_ancestors_to_tree_view(tree_store, default_person, parent_iter)
            self.add_descendants_to_tree_view(tree_store, default_person, parent_iter)

    def add_ancestors_to_tree_view(self, tree_store, person, parent_iter):
        """
        Add ancestors of a person to the TreeView.
        Args:
            tree_store: The TreeStore to add ancestors to.
            person: The person to add ancestors for.
            parent_iter: The parent iterator in the TreeStore.
        """
        for family_handle in person.get_parent_family_handle_list():
            family = self.dbstate.db.get_family_from_handle(family_handle)
            father_handle = family.get_father_handle()
            mother_handle = family.get_mother_handle()

            if father_handle:
                father = self.dbstate.db.get_person_from_handle(father_handle)
                dist = self.relationship.get_relationship_distance_new(
                    self.dbstate.db, self.dbstate.db.get_default_person(), father, only_birth=True)
                rank = dist[0][0]
                rel_a, rel_b = FamilyPathMetrics.extract_relationship_paths(dist)
                Ga, Gb = FamilyPathMetrics.calculate_relationship_path_lengths(rel_a, rel_b)
                mra = FamilyPathMetrics.calculate_mra(rel_a)
                kekule = FamilyPathMetrics.calculate_kekule_number(Ga, Gb, rel_a, rel_b)
                father_iter = tree_store.append(parent_iter, [
                    kekule,  # Rel_id
                    "Father",  # Relation
                    name_displayer.display(father),  # Name
                    Ga,  # up
                    Gb,  # down
                    mra,  # Common MRA
                    rank,  # Rank
                    get_timeperiod(self.dbstate.db, father.get_handle())  # Period
                ])
                self.add_ancestors_to_tree_view(tree_store, father, father_iter)

            if mother_handle:
                mother = self.dbstate.db.get_person_from_handle(mother_handle)
                dist = self.relationship.get_relationship_distance_new(
                    self.dbstate.db, self.dbstate.db.get_default_person(), mother, only_birth=True)
                rank = dist[0][0]
                rel_a, rel_b = FamilyPathMetrics.extract_relationship_paths(dist)
                Ga, Gb = FamilyPathMetrics.calculate_relationship_path_lengths(rel_a, rel_b)
                mra = FamilyPathMetrics.calculate_mra(rel_a)
                kekule = FamilyPathMetrics.calculate_kekule_number(Ga, Gb, rel_a, rel_b)
                mother_iter = tree_store.append(parent_iter, [
                    kekule,  # Rel_id
                    "Mother",  # Relation
                    name_displayer.display(mother),  # Name
                    Ga,  # up
                    Gb,  # down
                    mra,  # Common MRA
                    rank,  # Rank
                    get_timeperiod(self.dbstate.db, mother.get_handle())  # Period
                ])
                self.add_ancestors_to_tree_view(tree_store, mother, mother_iter)

    def add_descendants_to_tree_view(self, tree_store, person, parent_iter):
        """
        Add descendants of a person to the TreeView.
        Args:
            tree_store: The TreeStore to add descendants to.
            person: The person to add descendants for.
            parent_iter: The parent iterator in the TreeStore.
        """
        for family_handle in person.get_family_handle_list():
            family = self.dbstate.db.get_family_from_handle(family_handle)
            for child_ref in family.get_child_ref_list():
                child = self.dbstate.db.get_person_from_handle(child_ref.get_reference_handle())
                dist = self.relationship.get_relationship_distance_new(
                    self.dbstate.db, self.dbstate.db.get_default_person(), child, only_birth=True)
                rank = dist[0][0]
                rel_a, rel_b = FamilyPathMetrics.extract_relationship_paths(dist)
                Ga, Gb = FamilyPathMetrics.calculate_relationship_path_lengths(rel_a, rel_b)
                mra = FamilyPathMetrics.calculate_mra(rel_a)
                kekule = FamilyPathMetrics.calculate_kekule_number(Ga, Gb, rel_a, rel_b)
                child_iter = tree_store.append(parent_iter, [
                    kekule,  # Rel_id
                    "Child",  # Relation
                    name_displayer.display(child),  # Name
                    Ga,  # up
                    Gb,  # down
                    mra,  # Common MRA
                    rank,  # Rank
                    get_timeperiod(self.dbstate.db, child.get_handle())  # Period
                ])
                self.add_descendants_to_tree_view(tree_store, child, child_iter)

    def on_row_activated(self, tree_view, path, column):
        """
        Handle the "row-activated" signal for the TreeView.
        Args:
            tree_view: The TreeView widget.
            path: The path of the activated row.
            column: The column of the activated row.
        """
        # Get the selected row
        model = tree_view.get_model()
        iter = model.get_iter(path)
        handle = model.get_value(iter, 2)  # Get the handle from the third column

        # Get the person from the handle
        person = self.dbstate.db.get_person_from_handle(handle)

        # Open the person's edit dialog
        if person:
            self.uistate.set_active(person.handle, 'Person')

class FamilyTreeViewOptions(MenuItem):
    """
    Options for the FamilyTreeView.
    """
    def __init__(self, name, person_id=None):
        MenuItem.__init__(self, name, person_id)
        self.options_dict = {
            'enable_network_metrics': True,  # Option pour activer les métriques de réseau
        }
        self.options_help = {
            'enable_network_metrics': (
                _("Enable family network metrics"),
                "bool",
                _("Whether to calculate and display family network metrics."),
                None,
                True
            ),
        }

# Register the new view with Gramps
register_view(FamilyTreeView, FamilyTreeViewOptions)

# ... (rest of the existing code)
| Gramps Family Tree Viewer                                                                                           |
+---------------------------------------------------------------------------------------------------------------------+
| Rel_id | Relation       | Name            | up | down | Common MRA | Rank | Period      |
|--------+----------------+-----------------+----+------+-------------+------+-------------|
| 0      | Self           | John Doe        | 0  | 0    | 0           | 0    | 1900-2000   |
| 1      | Mother         | Jane Smith      | 1  | 2    | 15          | 1    | 1870-1950   |
| 2      | Mother         | Alice Johnson   | 2  | 3    | 30          | 2    | 1840-1920   |
| 3      | Father         | Bob Brown       | 2  | 3    | 30          | 2    | 1840-1920   |
| 4      | Father         | Michael Johnson | 1  | 2    | 15          | 1    | 1870-1950   |
| 5      | Mother         | Carol White     | 2  | 3    | 30          | 2    | 1840-1920   |
| 6      | Father         | David Black     | 2  | 3    | 30          | 2    | 1840-1920   |
+---------------------------------------------------------------------------------------------------------------------+


@GaryGriffin
Copy link
Member

Tried to run this on 6.0.5 on Mac. Gramps hung with:
40897: ERROR: relation_tab.py: line 410: No default person set.

Had to ctrl-c to kill it after it hung.

@GaryGriffin
Copy link
Member

Set the home person and it displayed a list. Selected 'Save' and gramplet crashed with error:

143095: ERROR: grampsapp.py: line 188: Unhandled exception
Traceback (most recent call last):
File "/Users/gary/Library/Application Support/gramps/gramps60/plugins/RelID/relation_tab.py", line 579, in button_clicked
self.save()
~~~~~~~~~^^
File "/Users/gary/Library/Application Support/gramps/gramps60/plugins/RelID/relation_tab.py", line 553, in save
filename = os.path.join(self.path, filename)
File "", line 77, in join
TypeError: expected str, bytes or os.PathLike object, not NoneType

When the gramplet started, it asked to select a folder. It never asked to select a filename.

@GaryGriffin
Copy link
Member

One surprise was that when I selected a Home Person and invoked the gramplet, it listed the relations in a gramps window AND in the console. I was not expecting all of this in the console.

@romjeromealt
Copy link
Contributor Author

Tried to run this on 6.0.5 on Mac. Gramps hung with: 40897: ERROR: relation_tab.py: line 410: No default person set.

Had to ctrl-c to kill it after it hung.

Thank you, Gary, for these tests.
I had problem for setting a default person via the CLI. Currently, the message has been added for a missing default person set (either via GUI or CLI), but it seems that I forgot to test it because it should not hung. This should be easily fixed via a simple return(). I will try to fix it. Thanks!

@romjeromealt
Copy link
Contributor Author

romjeromealt commented Sep 22, 2025

Set the home person and it displayed a list. Selected 'Save' and gramplet crashed with error:

143095: ERROR: grampsapp.py: line 188: Unhandled exception Traceback (most recent call last): File "/Users/gary/Library/Application Support/gramps/gramps60/plugins/RelID/relation_tab.py", line 579, in button_clicked self.save() ~~~~~~~~~^^ File "/Users/gary/Library/Application Support/gramps/gramps60/plugins/RelID/relation_tab.py", line 553, in save filename = os.path.join(self.path, filename) File "", line 77, in join TypeError: expected str, bytes or os.PathLike object, not NoneType

When the gramplet started, it asked to select a folder. It never asked to select a filename.

Do you mean the variable name? 👍
If so, it could be fixed by using 'foldername= os.path.join(self.path, filename)' then call 'foldername'... That's a cosmetic issue, I guess. I suppose that the 'NoneType' means a missing foldername, so maybe I should set the default $HOME folder by defaut? Otherwise, should I still ask for selecting a folder name? It is only for saving the tabular content to an OpenDocument file format with the 'default_person_ handle' as title. I am not a big fan of forcing a location, but maybe polishing means also to make it more simple as possible. So, this option for having a safe path location could be hidden before the run().

@romjeromealt
Copy link
Contributor Author

romjeromealt commented Sep 22, 2025

One surprise was that when I selected a Home Person and invoked the gramplet, it listed the relations in a gramps window AND in the console. I was not expecting all of this in the console.

oh, this could be a mixup... I was a little bit surprised to be able to get the ProgressMeter available via the CLI. Also, by playing with Gtk widgets (like window.show_all()) I tried to use an alternate windows and dialogs handling. I was not able to improve threading (Gtk or iteration with person_handle). This might be related to python 3.11 limitations but also I suppose to my lack on this domain... I need to make it more consistent againt others tools.

Someone reported that this addon takes more than 2 hours for 280 000 individuals... According to code, it should be more around 15 minutes with default configuration (except with 'deep max generations/levels' sets to 100) ! I supposed that an improvement could be added on such piece of code:

from gramps.gen.filters import GenericFilterFactory, rules
filter = FilterClass()
self.filter = FilterClass()
default_person = self.dbstate.db.get_default_person()
plist = self.dbstate.db.iter_person_handles()
if default_person: # rather designed for run via GUI...
    root_id = default_person.get_gramps_id()
    ancestors = rules.person.IsAncestorOf([str(root_id), True])
    descendants = rules.person.IsDescendantOf([str(root_id), True])
    related = rules.person.IsRelatedWith([str(root_id)])
    self.filter.add_rule(related)
    _LOG.info("Filtering people related to the root person...")
    self.progress.set_pass(_('Please wait, filtering...'))
    filtered_list = filter.apply(self.dbstate.db, plist)
    for handle in self.filtered_list:
       ...

As it only re-uses the filter rules, it will be difficult to modify something on this addon. Maybe just use it as a proof-of-concept on filtering slowdown, with large databases.

@romjeromealt
Copy link
Contributor Author

romjeromealt commented Sep 22, 2025

I was not expecting all of this in the console.

Maybe you are in the 'verbose' mode (aka. the debug)?

Instead of:
$ gramps -O 'example' -a tool -p name=relationtab -d "relation_tab"
you can try:
$ gramps -O 'example' -a tool -p name=relationtab

@romjeromealt
Copy link
Contributor Author

Tried to run this on 6.0.5 on Mac. Gramps hung with: 40897: ERROR: relation_tab.py: line 410: No default person set.

Had to ctrl-c to kill it after it hung.

I need to create a new empty Family Tree for having a database without default person. Maybe a very limited issue. Anyway, I am not sure for the fix. Maybe, something like:

else:
    _LOG.error("No default person set.")
    WarningDialog(_("No default_person"))
-   return
+   window.close()

@romjeromealt
Copy link
Contributor Author

I need to create a new empty Family Tree for having a database without default person. Maybe a very limited issue. Anyway, I am not sure for the fix. Maybe, something like:

else:
    _LOG.error("No default person set.")
    WarningDialog(_("No default_person"))
-   return
+   window.close()

oh yes, the WarningDialog will also pop-up via CLI!
About the main 'Gtk window' it should be exist via CLI.

File "/usr/lib/python3/dist-packages/gramps/gui/plug/tool.py", line 323, in cli_tool
callback=None,
File ".gramps/gramps52/plugins/RelID/relation_tab.py", line 412, in init
window.close()
AttributeError: 'NoneType' object has no attribute 'close'
Nettoyage.

:-(

I suppose you mean the WarningDialog?

@romjeromealt
Copy link
Contributor Author

Gtk-Message: 12:06:53.636: GtkDialog mapped without a transient parent. This is discouraged.
2025-09-22 12:06:53.636: ERROR: relation_tab.py: line 410: No default person set.
Gtk-Message: 12:06:53.652: GtkDialog mapped without a transient parent. This is discouraged.
Nettoyage.

Maybe I rather remove the WarningDialog instead of trying to look at transient Gtk parent widget on CLI...

@romjeromealt
Copy link
Contributor Author

Set the home person and it displayed a list. Selected 'Save' and gramplet crashed with error:

143095: ERROR: grampsapp.py: line 188: Unhandled exception Traceback (most recent call last): File "/Users/gary/Library/Application Support/gramps/gramps60/plugins/RelID/relation_tab.py", line 579, in button_clicked self.save() ~~~~~~~~~^^ File "/Users/gary/Library/Application Support/gramps/gramps60/plugins/RelID/relation_tab.py", line 553, in save filename = os.path.join(self.path, filename) File "", line 77, in join TypeError: expected str, bytes or os.PathLike object, not NoneType

I used "." for an empty path...
if self.path != '.':
I guess that a proper test is to use something like 'if not None'.

@romjeromealt
Copy link
Contributor Author

-if self.path != '.':
+if self.path is not None:

I have no idea how you can skip the folder path selection. I am trying to set an empty one or cancelling the selector, but no failure or traceback.

* add the copyright for the author of the filter rule related
* polish
* provide a quick documentation for the data displayed on the ProgressMeter
was a limit with old configurations (more than 10 years ago)
@GaryGriffin
Copy link
Member

Loaded the latest version of the PR. Ran via the GUI (no debug setting) on M3 Mac v 6.0.5. The Home Person was set. I invoked the gramplet and immediately got the Folder Chooser window. I selected the folder. I then got the list of relationships printed on the console as well as in a window.

I clicked Save and got an Error Dialog with:

61407: ERROR: grampsapp.py: line 188: Unhandled exception
Traceback (most recent call last):
  File "/Users/gary/Library/Application Support/gramps/gramps60/plugins/RelID/relation_tab.py", line 579, in button_clicked
    self.save()
    ~~~~~~~~~^^
  File "/Users/gary/Library/Application Support/gramps/gramps60/plugins/RelID/relation_tab.py", line 553, in save
    filename = os.path.join(self.path, filename)
  File "<frozen posixpath>", line 77, in join
TypeError: expected str, bytes or os.PathLike object, not NoneType

I never was asked to select a filename, only a folder.

Second Attempt:
This time I created a new tree and imported a ged file into it. There was no default person. When I ran the gramplet it opened the Folder Chooser window and when I selected, it crashed immediately with

Screenshot 2025-09-22 at 9 19 58 AM

When I canceled the Error Report, it opened the warning/error dialog with 'No default_person'. At this point, I could not cancel and had to go to the Console to ctrl-C to kill gramps.

Questions:

  1. Why is the Folder Chooser at the beginning? Shouldn't it be after clicking Save? If I Cancel the Folder Chooser, I still get the list of relationships and the option to Save. This seems strange. I was expecting it to abort the RelID if I canceled the Folder Chooser.
  2. Why am I never prompted for a filename to save to? Or a format?
  3. Should this be a Report instead of a Tool?
  4. Why is the list also output on the Console?
  5. If there is no default person, I thought it should generate a WarningDialog and then abort the RelID.

Observation:
I timed the gramplet and with my 18000 person tree, the filter step took 11 sec (I didnt have any filters enabled) and the calculate step took 44 sec. Overall 55 sec to find 454 relations.

@GaryGriffin
Copy link
Member

GaryGriffin commented Oct 3, 2025

Tested the most recent. These are the current issues that I see:

  1. Output still goes to Console.
  2. Folder selected is ignored. Save was to $cwd. Not sure where that would be if I clicked on icon to invoke gramps (not $HOME and not folder where Gramps located) rather than invoke from command line. Normally Gramps invoked from icon.
  3. Crashes if default_person not set.
  4. Needs a Close button on the output window (beside the Save button).

@romjeromealt
Copy link
Contributor Author

romjeromealt commented Oct 3, 2025

Capture d’écran de 2025-10-03 20-43-10 Is it the most recent? efb5730 You should have a `Quit` button, which is not far away to `Close`.

@romjeromealt
Copy link
Contributor Author

romjeromealt commented Oct 3, 2025

  1. Needs a Close button on the output window (beside the Save button).

Ah, ok, like on most dialogs into gramps, the common Gtk buttons (valid and cancel)?

That's the hbox/vbox stuff from Gtk3... :-(
It becomes too complex for a simple addon. There is no real set of widgets designed for tools, isn't it?

@romjeromealt
Copy link
Contributor Author

  1. Crashes if default_person not set.

I cannot reproduce it, either via cli or gui, by creating an empty Family Tree and run the tool. It raises a message or Warning Dialog, but no crashes.

@GaryGriffin
Copy link
Member

Is it the most recent? https://github.com/gramps-project/addons-source/commit/efb5730bcd764b82f8798a91f2b53a4cde8f0c10 You should have a Quitbutton, which is not far away toClose.

No, there is no Close button in the latest on the Mac
Screenshot 2025-10-03 at 11 48 19 AM

Also, in the FolderChooser window, if I press Cancel, what actually happens? The gramplet does its calculation and output. What is different from the Cancel and the Select in this window?

@GaryGriffin
Copy link
Member

Crashes if default_person not set.
I cannot reproduce it, either via cli or gui, by creating an empty Family Tree and run the tool. It raises a message or Warning Dialog, but no crashes.

Let me clarify, I run with the gui and create a new family tree and import a ged (Shakespeare in my case). If I then run the gramplet it generates the error
ERROR: relation_tab.py: line 411: No default person set.
and gramps is hung. I cannot close any window. I have to ctrl-c the app to kill it.

@GaryGriffin
Copy link
Member

4. Needs a Close button on the output window (beside the Save button).
Ah, ok, like on most dialogs into gramps, the common Gtk buttons (valid and cancel)?

That's the hbox/vbox stuff from Gtk3... :-(
It becomes too complex for a simple addon. There is no real set of widgets designed for tools, isn't it?

You are right that it is starting to get complex. If I take a step back, my concern is that I get a window with a Save button. If I press the Save, I dont know if anything happened - the window is still open after pressing. I was expecting either pressing the Save to close the window upon completion or to have a close button.

The only way to close this window is to use the window manager to close it.

@romjeromealt
Copy link
Contributor Author

ERROR: relation_tab.py: line 411: No default person set.

line 411 is currently this one:

def apply_and_update_filter(self):
       """Applique le filtre et met à jour l'UI."""
       filter_start = time.perf_counter()
->     plist = list(self.dbstate.db.iter_person_handles())
       self.filtered_list = self.filter_manager.apply_filter(plist)

you should get it on line 363!

I suppose that you have at least two versions (to have a 'twice' copy of the plugin into your plugin directory)?

@romjeromealt
Copy link
Contributor Author

I should be able to call the basic gtk widgets or built-in dialogs like:

from gramps.gui.dialog import WarningDialog, OkDialog
if uistate:
    self.progress = ProgressMeter(self.label, can_cancel=False, parent=uistate.window)
    ...
    OkDialog(
    _("Please, wait..."),
    _("Run in progress."),
    parent=uistate.window,
     )
    ...
    WarningDialog(_("No default person set."), parent=uistate.window)

@GaryGriffin
Copy link
Member

My error. I recloned the addons and am now testing the latest version on MacOS. Sorry for the confusion.

  • Default_person not set: fixed
  • Quit button: fixed
  • Output to Console: fixed
  • Save button: brings up FolderChooser window. After selecting a folder and pressing Select, I get a Did you set a foldername ErrorMsg (Cannot set a valid location).

Save button seems to be the only open issue on the Mac. Has someone tested on Linux? Win?

@romjeromealt
Copy link
Contributor Author

romjeromealt commented Oct 6, 2025

You are right, I can reproduce the select folder issue under linux too. I did something wrong. The Current Working Directory (CWD) behavior does not really need a folder selector. Maybe I did a mixup during tests and fixes.

About $ git merge --squash or so, for getting only one commit. Can you merge it with this option from gramps-addons side?

tweak improvement by using the information dialog
@GaryGriffin
Copy link
Member

Tried the latest (with getcwd change). If default_person not set, it does not bring up FolderSelector window, and does not calculate relationships. Good

At this point, things look good except the Save does not work (FolderChooser.get_current_folder() always returns None).

re: merge - it is fine to leave as multiple commits.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants