Skip to content

Sync addons-source@maintenance/gramps61 with upstream (2026-07-10) - #61

Open
eduralph wants to merge 155 commits into
maintenance/gramps61from
sync/upstream-maintenance-gramps61-auto
Open

Sync addons-source@maintenance/gramps61 with upstream (2026-07-10)#61
eduralph wants to merge 155 commits into
maintenance/gramps61from
sync/upstream-maintenance-gramps61-auto

Conversation

@eduralph

Copy link
Copy Markdown
Owner

Automated nightly sync from gramps-project/addons-source@maintenance/gramps61. Generated by .github/workflows/upstream-sync.yml on the testbed.

dsblank and others added 30 commits July 9, 2026 08:32
Ships 10 worked example .gram.py scripts in a shared scripts/ folder that
also serves as the default Open/Save location, so a user's own scripts
naturally collect next to them. Descriptions live in a translatable
script_descriptions.py module (picked up by the addon's existing xgettext
pipeline) and are shown as a preview when browsing the Open dialog, with
a fallback to a script's own leading comment for uncatalogued files.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Places it directly below the script editor instead of below the
Table/Output/Chart tabs, so it's closer to where the script is written.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds a persistent filename label on the left of the status bar (split
from the transient status message via an HBox), updated whenever a
script is loaded or saved, so it's always clear which script is loaded.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The filename label was using the same bordered style as the status
message, making the two indistinguishable. Give it its own bold,
borderless style and more room from the status text next to it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Uses the buffer's built-in modified flag plus Gramps' standard
SaveDialog (Save / Don't Save / Cancel) so New and Open no longer
silently discard in-progress edits.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Moves get_columns()/extract_header_comment() into a new script_utils.py
(no GTK/Gramps imports) so both GrampyScript.py and this new dev tool can
share them, and so the two tests exercising them no longer need the
ast-extraction workaround.

update_script_descriptions.py scans scripts/*.gram.py and keeps
SCRIPT_DESCRIPTIONS in script_descriptions.py in sync: adds a stub entry
for new scripts, drops entries for deleted ones, and warns (without
overwriting) when a file's title comment has drifted from the catalogued
title. Existing entries' exact source text is preserved via ast slicing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Scripts can now import plain .py helper modules placed next to them
(scripts dir and the open script's own dir are added to sys.path before
exec), reuse an existing Gramps sidebar custom filter via custom_filter(),
and remove an object via delete() instead of needing to know the
per-class remove_* db call. Also fixes active_event to return a
DataDict2 like every other active_* constant, instead of a raw handle.

Adds three example scripts (and a script_helpers.py helper module) to
scripts/, catalogued in script_descriptions.py and scripts/README.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Wires a Tab-triggered, live-filtering completion popover into the
script editor, covering plain Python names, the DSL's own functions
(people(), custom_filter(), selected()/filtered(), ...), and nested
attribute chains on Gramps records (person.primary_name.first_name),
including through a user's own loop variables and list subscripts.

completion.py wraps jedi.Interpreter for the actual lookups.
stub_generator.py derives static type stubs straight from Gramps'
own get_schema() so jedi can infer generator/loop-variable row types
without ever executing anything (a live template object would
require calling DataDict2's computed properties, e.g. father/birth,
which only degrade to empty results for blank data anyway).
namespace_builder.py supplies the remaining live objects (today,
counter, database) that are safe to introspect directly.
completion_popup.py is a standalone Gtk.Popover controller, kept
independent of the Gramplet class so it's testable against a plain
Gtk.TextView. DataDict2 gained a __dir__ override so introspection
(jedi's runtime fallback) sees dynamic dict keys like primary_name,
not just its declared properties.

Also fixes the editor ScrolledWindow/TextView having no wrap mode or
explicit scroll policy, which let long lines widen the whole
gramplet instead of scrolling within it.

Requires jedi (added to GrampyScript.gpr.py's requires_mod), which
ships with Gramps 6.1.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…tainer

statusmsg's text sometimes embeds the current file's full path (e.g.
"Loaded '/home/.../scripts/some_script.gram.py'"), and an unbounded
Gtk.Label requests enough natural width to fit that whole string,
which was pushing the gramplet wider than its panel and forcing
horizontal scrolling. Capping it with set_ellipsize()/
set_max_width_chars() bounds the natural width regardless of message
content.

Also reverts the wrap-mode/scroll-policy change from the previous
commit, which guessed the code editor's TextView was the cause; it
wasn't, and auto-wrapping code isn't desirable anyway since it breaks
visual alignment of indentation.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Opens the addon's wiki help page (Addon:GrampyScript), reusing the
same help_url the gramplet already exposes via self.gui.
Each scripts/*.gram.py file now carries its own description as a module
docstring; script_descriptions.py is fully regenerated from those
docstrings (plus each file's title comment) via update_script_descriptions.py,
instead of hand-maintaining translated text disconnected from the examples
it describes. Also fix the editor's syntax highlighter, which had no notion
of string literals and was bolding keywords found inside quoted strings
(most visibly inside the new docstrings).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Completions for callables (people(), families(), custom_filter(), dict
methods, etc.) now insert with parens, landing the cursor between them
when the function takes arguments.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Why show a dropdown with nothing to choose between? trigger() now
inserts directly when there's exactly one candidate, falling back to
the popover only when there's a real choice to make.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
age was typed as "object" even though DataDict2.age actually returns a
gramps.gen.lib.date.Span (from Date - Date), so jedi had nothing to
complete on it. back_references/back_references_recursively had the
same "object" placeholder, which is worse than useless since jedi
can't iterate a bare object at all -- completions on their loop items
returned nothing. Both are now typed precisely: age as Span (imported
into the stub preamble), and the back-reference properties as a union
of every row type, mirroring the existing selected()/filtered() trick.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
COMPUTED_PROPERTIES was a single flat dict layered onto every schema
class, including nested structural types (Name, Attribute, ...), so
the editor offered fields like father/spouse/gender/age everywhere --
even where the underlying DataDict2 property would raise (gender on a
non-Person) or silently do nothing. It's now name -> (type, valid
root types), and build_registry() only attaches a property to the
root record types it's actually valid on. `reference` moves out
entirely, onto the nested *Ref wrapper types it actually belongs to.

Also fixes two real datadict2.py bugs the audit turned up:
surname/name used unguarded self["surname"]/self["name"], raising
KeyError on any class without that field; reference always called
get_raw_person_data regardless of which *Ref type it was wrapping.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
get_completions() returned bare function names (e.g. "people",
"as_age") while get_completion_items() appended "()" to the same
completions -- inconsistent, and a bare name reads as a field rather
than a callable. Both now share a _display_name() helper so callables
agree everywhere.

Also exclude jedi type "class" completions altogether: the stub
preamble injects scaffold classes (Person, Family, ...) purely for
static analysis, and they aren't bound to anything in the namespace a
script actually executes in, so offering them as completions would
suggest names that raise NameError if accepted. Builtin classes
(list, dict, ...) are dropped too, since the DSL has no use for
instantiating classes directly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…lename

The status message duplicated the filename already shown by
filename_label, so drop those redundant messages and use the freed-up
space to surface the Tab-completion shortcut. Also fix New leaving the
old filename label in place, which caused Save to silently overwrite
the previous file instead of prompting Save As.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
columns, begin_changes, end_changes, delete, row, and chart are all
top-level DSL callables bound as local closures inside execute_code(),
so jedi never saw them since they weren't part of the completion stub
or namespace. Adds a VOID_FUNCTIONS entry in stub_generator.py that
renders their signatures as "-> None" completions.
open(path).read() without a context manager leaves the file handle
open until garbage collected, which triggers ResourceWarning under
python -m unittest. Use with-blocks in the four spots that did this.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A nested wrapper's _object was rebuilt via data_to_object() from just
its own dict slice, disconnected from the real object tree. Calling a
set_*() method (e.g. surname.set_origintype()) mutated that throwaway
clone, then the commit step re-serialized the untouched real object,
so the change never reached the database.

Now nested wrappers resolve _object by walking the root's real object
via self.path, so set_*() calls (and attribute assignment) mutate the
actual object that gets persisted.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
[dd.primary_name] + dd.alternate_names goes through DataList2.__radd__,
producing a DataList2 whose elements are already DataDict2/DataList2
instances. __getitem__ unconditionally re-wrapped dict/list values, and
since those wrapper classes subclass dict/list, it re-wrapped already-
wrapped items too -- discarding their real root/path and substituting
this list's own (often None, defaulting to self) root. That produced a
DataDict2 whose root was itself but whose path was non-empty, an
inconsistent state that made attribute assignment recurse forever
trying to resolve self.root._object.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… the label

The raw serialized "string" field of a GrampsType value (NameOriginType,
NameType, EventType, ...) is only the *custom*-type override text -- it
is always "" for predefined values like PATRILINEAL. Since DataDict2's
generic dict-key lookup returned that raw field directly, `.string`
looked empty even after setting a real origin type.

Add a `string` property that, when the wrapped value is a GrampsType,
returns the actual computed label (str(the_type)) instead. Falls back
to normal attribute lookup for anything without a "string" field, so
unrelated objects are unaffected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
`names` was `[self.primary_name] + [self.alternate_names]` -- the extra
brackets around alternate_names nested the whole list as a single
element instead of spreading its items in.

Separately, DataList2.__radd__ returned `self + value` instead of the
mathematically required `value + self` (Python calls b.__radd__(a) to
compute `a + b`), so `plain_list + data_list2` -- the exact pattern
used to loop over primary + alternate names -- came out reversed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
`dl.set_privacy(True)` fanned out attribute access first, collecting
each item's unevaluated set_*() wrapper closure into a DataList2 --
then failed to call, since a DataList2 of closures isn't callable
itself. Special-case set_*() the same way DataDict2 already does: return
one callable that applies the same args to every item in the list.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
begin_changes()/end_changes() called the lowlevel db._txn_begin()/_txn_commit()
(raw SQL BEGIN/COMMIT) instead of db.transaction_begin()/transaction_commit(),
so the DbTxn was never pushed onto undodb and script edits were invisible to
Undo/Redo despite being written to disk.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
PR gramps-project#978 added custom_filter(name, namespace="Person") and delete(obj) to
GrampyScript's execution scope. GrampsAssistant drives that same scope via
tools.py's execute_script/evaluate_expression docstrings, so the model
needs to know these exist to use or suggest them.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@eduralph
eduralph force-pushed the sync/upstream-maintenance-gramps61-auto branch from 0fc7d61 to f88aa53 Compare August 7, 2026 05:26
dsblank and others added 15 commits August 7, 2026 15:26
…ckend

Lets Gramps open a gramps-web-api server (e.g. gramps-connect, Gramps Web)
as a regular family tree -- read and write, no export/import step.
Subclasses the stock SQLite DBAPI backend rather than reimplementing
DbReadBase/DbWriteBase, and keeps a local mirror in sync incrementally via
the server's transaction-history endpoint; local edits push back through
transaction_commit(). Credentials come from a single GRAMPS_WEB_API_KEY
env var (a non-expiring refresh token) rather than a login dialog, which
also makes the same webapi_client.py usable as a bare SDK outside Gramps.

Status UNSTABLE: no conflict handling (writes are last-write-wins by
design, not yet), no undo/redo integration, no media sync. Verified
end-to-end against a live gramps-web-api server, including live use from
Gramps desktop itself, but no automated test suite yet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…radeoff

Explains that GRAMPS_WEB_API_KEY carries a standard, non-expiring
refresh token from the server's normal login flow rather than a
scoped/revocable personal access token, so a leaked key is as
damaging as a leaked password until it's changed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Cover webapi_client.WebApiHandler (token codec, JWT decoding, auth flows,
429/401 retry and API-prefix fallback, transaction-history/push request
shape) and grampswebapidb.WebApiDB (transaction_to_json, _apply_change,
_sync_from_server pagination, transaction_commit ordering and error
handling). No real server or SQLite file is needed; urlopen and the
DBAPI/SQLite base are stubbed throughout.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Drop force=1 from POST /transactions/ so the server's old-data-mismatch
check actually runs. A rejected push now raises WebApiPushConflict
(webapi_client.py), which transaction_commit() catches separately from
generic connection errors: it logs a distinct warning and resyncs from
the server so the local mirror stops showing an edit the server never
accepted, rather than drifting silently. webapi_client.py's docstring
also now notes it's a hand-synced vendored copy of the standalone
gramps-web-api-client package.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…y CLI

Documents the standalone package's CLI as the primary way to mint a
GRAMPS_WEB_API_KEY, with the addon's own vendored
WebApiHandler.mint_api_key() as the equivalent no-extra-dependency
fallback.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…-api-client rename

The standalone client package was renamed (gramps_web_api_client ->
gramps_api_client, new checkout at ~/gramps/gramps-api-client). Updated
webapi_client.py's docstring and README.md accordingly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ncing it

Gramps core's DbGenericUndo._undo()/_redo() revert the local mirror via
low-level _txn_begin()/undo_data()/_txn_commit() calls that never go
through transaction_commit(), so a local Undo/Redo previously left the
server unchanged with no error at all -- worse than a push conflict,
since nothing was even logged.

WebApiDB now overrides undo()/redo(): both grab the relevant DbTxn off
DbGenericUndo's queue before delegating to super(), rebuild its payload
with the existing transaction_to_json(), and push it. Undo sends it to
POST /transactions/?undo=1, where gramps-web-api reverses it itself
(reverse_transaction()); redo just re-pushes the original forward
payload, same as an ordinary commit. Both share the same
conflict-detection/resync path as transaction_commit(), factored out
into _push_payload().

Verified end-to-end against a live server: add a person, undo (a fresh
mirror sync confirms the server no longer has it), redo (confirms it's
back).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- grampswebapidb.py: use the documented try/except get_addon_translator
  fallback (glocale.translation directly if the addon has no locale/
  translations yet) instead of a bare glocale.translation.gettext.
- grampswebapidb.gpr.py: add help_url pointing at the addon's wiki page.
- MANIFEST: include README.md in the built .addon.tgz -- it documents
  the GRAMPS_WEB_API_KEY security tradeoff, not just dev notes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ommit gap

A batch=True commit (any bulk import, merge, or tool run through
gramps-web-api) leaves an empty-changes marker in the transaction
history instead of per-object entries, since DBAPI's commit/remove
methods skip the undo-log call for batch transactions. _sync_from_server()
had no way to detect this and silently missed everything the batch
commit did -- confirmed live: importing example.gramps's 2157 people
into a synced tree left the local mirror stuck at its pre-import count
indefinitely, no matter how often it resynced.

_sync_from_server() now treats an empty-changes transaction as a
signal, not a no-op, and falls back to a new _full_resync(): download
the server's current Gramps XML export and reimport it (via the same
stock ImportXml the batch commit itself used) into a freshly wiped
local mirror.
…live

load() now schedules a periodic re-sync (GLib.timeout_add_seconds), matching
gramps-connect's own browser-client poll against the same endpoint, so edits
made from another client show up here without closing and reopening the
tree; close() cancels the pending timeout. Since _sync_from_server()'s
replay runs in a batch=True DbTxn, DBAPI's own add/update/delete signals
never fire for it -- _emit_change_signals() reproduces them by hand so
already-open views refresh the same way they would for a local edit, and
_full_resync() now calls request_rebuild() for the same reason on its
wipe-and-reimport path.
Previously a rejected push (someone else changed the object server-side
since the local mirror last synced) just resynced from the server and
dropped the local edit. Now it retries the edit on top of the resynced
data, merging list-valued fields (notes, citations, events, ...) via the
object's own merge() -- the same logic behind Gramps' Merge People/
Family/... tools, ported from GrampsWebSync's diffhandler.py -- instead
of one side blindly overwriting the other. A second conflict on the
retry itself is logged and dropped rather than retried again.
…report sync progress

Nothing tied a local mirror to a particular GRAMPS_WEB_API_KEY account:
switching the env var to a different server/tree while reopening the
same Family Tree left the mirror silently mixing stale data from the
old account with whatever the incremental sync happened to pull from
the new one. load() now calls _check_identity(), which requires the
Family Tree's own name to be "<username>@<host>" for whoever the key
currently authenticates as (normalized through the same character
substitution Gramps' own Family Tree Manager applies when renaming a
tree, since a hostname's dots can never survive that GUI round-trip
intact) -- a mismatch fails DbConnectionError instead of loading.

load()'s callback parameter (the same hook Gramps wires to its real
progress bar) is now threaded through to _sync_from_server(), which
reports page-by-page percent-complete using the server's X-Total-Count,
and to _full_resync(), which gets coarse 0/100 markers since ImportXml
has no internal step reporting to forward finer progress from. A slow
initial catch-up now shows real progress instead of Gramps looking hung.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Core sync/write-through/undo-redo/conflict-detection paths are verified
against a live server and covered by tests; remaining gaps (media file
sync, scalar-field conflict resolution) are documented limitations
rather than missing basics. template.pot was stale, missing strings
added by the last commit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@eduralph
eduralph force-pushed the sync/upstream-maintenance-gramps61-auto branch 2 times, most recently from 93d4dc3 to 65de30a Compare August 9, 2026 04:53
dsblank and others added 4 commits August 9, 2026 10:02
…e its Family Tree

Minting a key previously required a shell with gramps-api-client
installed, or hand-writing the three lines of Python from the addon's
README. Tools -> Utilities -> Generate Gramps Web API key now turns a
server URL/username/password into a key from a form, sets it in the
running Gramps process's environment so a WebApiDB-backed tree can be
opened without restarting, and offers a follow-up button to create a
correctly-named, correctly-backed Family Tree for that key via the same
CLIDbManager API Gramps' own Family Tree Manager uses.

Also tightens grampswebapidb.py's own error handling: load()'s initial
sync is now wrapped the same way _check_identity() already was, so a
failure there raises the addon's DbConnectionError instead of escaping
as a raw HTTPError that Gramps' generic dbloader dialog shows unhelpfully;
and HTTP 403s specifically get a message calling out that it's a
permissions problem with the authenticated account, not a URL/credentials
one.
Gramps only populates the Tools menu once some Family Tree is open, so
the "Generate Gramps Web API key" tool still needs one open (any
backend) to be reachable, even though it doesn't touch that tree's
data. README.md and the tool's own module docstring said otherwise.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… create its Family Tree#1012

Also update template.pot
@eduralph
eduralph force-pushed the sync/upstream-maintenance-gramps61-auto branch 3 times, most recently from d4cbcfe to b35f1c7 Compare August 12, 2026 05:22
dsblank and others added 3 commits August 12, 2026 15:56
The exporter writes one JSON object per line (JSON Lines format), not
a single JSON document, so the file extension should be .jsonl rather
than .json.
@eduralph
eduralph force-pushed the sync/upstream-maintenance-gramps61-auto branch from b35f1c7 to df2d04b Compare August 13, 2026 05:26
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.