Skip to content

Commit c1103b4

Browse files
Sync addons-source@maintenance/gramps60 with upstream/gramps-project (2026-05-23)
2 parents 73a1808 + ff6d388 commit c1103b4

12 files changed

Lines changed: 156 additions & 13 deletions

GrampsWebSync/grampswebsync.gpr.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
id="gramps_web_sync",
2929
name=_("Gramps Web Sync"),
3030
description=_("Synchronizes a local database with a Gramps Web instance."),
31-
version = '1.3.10',
31+
version = '1.3.11',
3232
gramps_target_version="6.0",
3333
status=STABLE,
3434
fname="grampswebsync.py",

GrampsWebSync/grampswebsync.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ def __init__(self, dbstate, user, options_class, name, *args, **kwargs) -> None:
180180

181181
self.db1 = dbstate.db
182182
self.db2 = None
183+
self._closing = False
183184
self._download_timestamp = 0
184185
self._changes: Actions | None = None
185186
self._sync: WebApiSyncDiffHandler | None = None
@@ -213,6 +214,14 @@ def build_menu_names(self, obj): # type: ignore
213214
def do_close(self, assistant):
214215
"""Close the assistant."""
215216
LOG.debug("Closing Gramps Web Sync addon.")
217+
self._closing = True
218+
if self.db2 is not None:
219+
LOG.debug("Closing in-memory remote database.")
220+
self.db2.close()
221+
self.db2 = None
222+
# Clear the diff handler which holds references to both db1 and db2
223+
self._sync = None
224+
self._changes = None
216225
position = self.window.get_position() # crock
217226
self.assistant.hide()
218227
self.window.move(position[0], position[1])
@@ -500,6 +509,8 @@ def async_compare_dbs(self):
500509

501510
def get_diff_actions(self) -> None:
502511
"""Download the remote data, import it and compare it to local."""
512+
if self._closing:
513+
return
503514
LOG.info("Downloading Gramps XML file.")
504515
path = self.handle_server_errors(self.api.download_xml)
505516
if path is None:
@@ -545,6 +556,8 @@ def async_transfer_media(self):
545556

546557
def _async_transfer_media(self):
547558
"""Upload/download media files."""
559+
if self._closing:
560+
return
548561
self.handle_server_errors(self.download_files)
549562
if self.conclusion.error:
550563
return
@@ -665,6 +678,8 @@ def _async_commit_actions_to_remote(
665678
self, payload: dict[str, "Any"], force: bool
666679
) -> None:
667680
"""Upload/download media files."""
681+
if self._closing:
682+
return
668683
LOG.debug("Committing changes to remote database.")
669684
self.handle_server_errors(
670685
self.api.commit,

GrampyScript/GrampyScript.gpr.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
name=_("Gram.py Script"),
2424
description=_("Run a special Gramps Python script"),
2525
status=STABLE,
26-
version = '0.0.2',
26+
version = '0.0.3',
2727
fname="GrampyScript.py",
2828
authors=["Doug Blank"],
2929
authors_email=["doug.blank@gmail.com"],

GrampyScript/datadict2.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ def flatten(lst):
4545
result.append(item)
4646
return result
4747

48+
4849
class NoneData:
4950

5051
# def __setattr__(self, attr, value):
@@ -68,6 +69,9 @@ def __call__(self, *args, **kwargs):
6869
# print(args, kwargs)
6970
return ""
7071

72+
def __iter__(self):
73+
return iter([])
74+
7175

7276
class DataDict2(dict):
7377
"""
@@ -234,7 +238,9 @@ def tags(self):
234238
def citations(self):
235239
return DataList2(
236240
[
237-
DataDict2(sa.dbase.get_raw_citation_data(handle), callback=self.callback)
241+
DataDict2(
242+
sa.dbase.get_raw_citation_data(handle), callback=self.callback
243+
)
238244
for handle in self.citation_list
239245
],
240246
)
@@ -252,6 +258,10 @@ def events(self):
252258
],
253259
)
254260

261+
@property
262+
def reference(self):
263+
return DataDict2(sa.dbase.get_raw_person_data(self.ref), callback=self.callback)
264+
255265
@property
256266
def attributes(self):
257267
return self.attribute_list
@@ -268,6 +278,22 @@ def lds_ords(self):
268278
def references(self):
269279
return self.person_ref_list
270280

281+
@property
282+
def back_references(self):
283+
retval = []
284+
for obj_type, ohandle in sa.dbase.find_backlink_handles(self.handle):
285+
obj = sa.dbase.method("get_%s_from_handle", obj_type)(ohandle)
286+
retval.append(DataDict2(obj, callback=self.callback))
287+
return DataList2(retval)
288+
289+
@property
290+
def back_references_recursively(self):
291+
retval = []
292+
for obj_type, ohandle in self._object.get_referenced_handles_recursively():
293+
obj = sa.dbase.method("get_%s_from_handle", obj_type)(ohandle)
294+
retval.append(DataDict2(obj, callback=self.callback))
295+
return DataList2(retval)
296+
271297
@property
272298
def name(self):
273299
if self["_class"] == "Person":

LinesOfDescendency/lines-of-descendency.gpr.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
"Prints out all descendency lines "
3434
"from a given ancestor to a given descendent in text."
3535
),
36-
version = '1.1.40',
36+
version = '1.1.41',
3737
gramps_target_version="6.0",
3838
status=STABLE, # not yet tested with python 3
3939
fname="lines-of-descendency.py",

LinesOfDescendency/lines-of-descendency.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,14 @@ def write_path(self, path):
126126
mother if mother != handle \
127127
else family.get_father_handle()
128128
handle = next_handle
129-
spouse = self.database.get_person_from_handle(spouse_handle)
129+
# Bug 12913: a family may have only one parent defined,
130+
# in which case spouse_handle is None and
131+
# get_person_from_handle raises HandleError. Guard the
132+
# lookup so the existing "N.N." fallback still applies.
133+
if spouse_handle:
134+
spouse = self.database.get_person_from_handle(spouse_handle)
135+
else:
136+
spouse = None
130137
if spouse:
131138
spouse_name = _nd.display(spouse)
132139
else:

LinesOfDescendency/tests/test_linesofdescendency_guards.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,5 +127,100 @@ def test_both_present_does_not_raise(self):
127127
self.assertIsNotNone(report.ancestor)
128128

129129

130+
class TestWritePathMissingSpouse(unittest.TestCase):
131+
"""Bug 12913: ``write_path`` raised ``HandleError: Handle is None``
132+
when a family in the descent chain had only one parent defined
133+
(so ``spouse_handle`` came back as ``None``) and the addon called
134+
``get_person_from_handle(None)`` directly. The guard added in
135+
this PR catches the ``None`` before the lookup and falls through
136+
to the existing ``'N.N.'`` spouse-name fallback that was already
137+
in place for the "spouse object missing" case."""
138+
139+
@classmethod
140+
def setUpClass(cls):
141+
try:
142+
cls.impl = _load_impl()
143+
except ImportError as err:
144+
raise unittest.SkipTest("Gramps not importable: %s" % err)
145+
cls.LinesOfDescendency = cls.impl.LinesOfDescendency
146+
147+
@staticmethod
148+
def _report_instance(database, doc):
149+
"""Build a ``LinesOfDescendency`` instance via ``__new__`` so
150+
``Report.__init__`` (which we don't need) doesn't run, and
151+
wire up the minimum attributes ``write_path`` touches."""
152+
report = TestWritePathMissingSpouse.LinesOfDescendency.__new__(
153+
TestWritePathMissingSpouse.LinesOfDescendency
154+
)
155+
report.database = database
156+
report.doc = doc
157+
report.line = 1
158+
return report
159+
160+
@staticmethod
161+
def _person(handle, gramps_id, parents_family_handle):
162+
"""Mock person with the minimal API ``write_path`` touches."""
163+
person = mock.MagicMock()
164+
person.get_handle.return_value = handle
165+
person.get_gramps_id.return_value = gramps_id
166+
person.get_main_parents_family_handle.return_value = (
167+
parents_family_handle
168+
)
169+
return person
170+
171+
@staticmethod
172+
def _family(mother_handle, father_handle, relationship):
173+
family = mock.MagicMock()
174+
family.get_mother_handle.return_value = mother_handle
175+
family.get_father_handle.return_value = father_handle
176+
family.get_relationship.return_value = relationship
177+
return family
178+
179+
def test_single_parent_family_does_not_raise(self):
180+
"""The reporter's case: a family in the chain has only one
181+
parent defined. Pre-fix, ``get_person_from_handle(None)``
182+
raised ``HandleError: Handle is None`` and torpedoed the
183+
whole report."""
184+
# Chain: ancestor (handle "A") -> child (handle "C")
185+
# Family F0037 has father "A" and mother = None ("Reeves, Maria"
186+
# removed per reporter's repro). When traversing from ancestor
187+
# to child, spouse_handle resolves to None.
188+
ancestor = self._person("A", "I0001", parents_family_handle=None)
189+
child = self._person("C", "I0055", parents_family_handle="F0037")
190+
# Family with only the father, no mother:
191+
family = self._family(
192+
mother_handle=None,
193+
father_handle="A",
194+
relationship=self.impl.FamilyRelType.MARRIED,
195+
)
196+
197+
database = mock.MagicMock()
198+
database.get_person_from_handle.side_effect = lambda h: {
199+
"A": ancestor,
200+
"C": child,
201+
}[h]
202+
database.get_family_from_handle.return_value = family
203+
204+
doc = mock.MagicMock()
205+
report = self._report_instance(database, doc)
206+
207+
# Pre-fix this would raise HandleError; post-fix it must
208+
# complete and never call get_person_from_handle with None.
209+
report.write_path(["A", "C"])
210+
211+
# Verify get_person_from_handle was NEVER called with None
212+
# -- the whole point of the guard.
213+
for call in database.get_person_from_handle.call_args_list:
214+
self.assertIsNotNone(call.args[0])
215+
216+
# Verify the user-visible output still uses the existing
217+
# 'N.N.' fallback for the missing-spouse case.
218+
written_calls = [str(c) for c in doc.write_text.call_args_list]
219+
self.assertTrue(
220+
any("N.N." in text for text in written_calls),
221+
"Expected 'N.N.' fallback in rendered text; got %r" % written_calls,
222+
)
223+
224+
130225
if __name__ == "__main__":
131226
unittest.main()

PostgreSQLEnhanced/postgresqlenhanced.gpr.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
"and AI/ML capabilities. For advanced users. "
3434
"Requires PostgreSQL 15+ with extensions. Gramps Web compatible."
3535
),
36-
version = '1.5.2',
36+
version = '1.5.3',
3737
gramps_target_version="6.0",
3838
status=STABLE,
3939
audience=EXPERT, # For advanced users who can configure PostgreSQL

PostgreSQLEnhanced/postgresqlenhanced.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import re
4141
import pickle
4242
import sys
43+
import time
4344
from urllib.parse import urlparse, parse_qs
4445

4546
# -------------------------------------------------------------------------
@@ -1958,7 +1959,6 @@ def _set_metadata(self, key, value, use_txn=True):
19581959
pass
19591960

19601961
if attempt < max_retries - 1:
1961-
import time
19621962
time.sleep(0.01 * (2 ** attempt)) # Exponential backoff
19631963
continue
19641964
else:

lxml/etreeGramplet.gpr.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
description=_("Gramplet for testing etree with Gramps XML"),
1212
status=EXPERIMENTAL,
1313
audience = DEVELOPER,
14-
version = '1.2.2',
14+
version = '1.2.3',
1515
gramps_target_version="6.0",
1616
include_in_listing=True,
1717
height=400,

0 commit comments

Comments
 (0)