28
edits
Changes
From Gramps
Synced from repo@41d611f via publish.py
<!--
Cross-cutting concerns every addon author hits regardless of kind.
Ordered by the sequence an author actually meets them, not
alphabetically. Authoritative cross-references inside the manual:
- per-kind specifics -> 04-addon-kinds
- DB API surface -> 06-data-access
- testing -> 08-testing
- failure modes -> 10-troubleshoot
-->
== Overview ==
The cross-cutting concerns every addon author hits regardless of which kind they're building. If something in a kind-specific page assumes a piece of background, it's described here.
[[File:plugin-discovery.svg|Fig. 1 — Plugin discovery and load sequence. Gramps scans the plugin directory at startup, executes each <code>register()</code> call into a metadata-only catalog, and loads the implementation module lazily when the user first invokes the addon.]]
Note that the catalog → invoke arrow is dashed: addon implementation modules are ''not'' loaded at startup. The <code>.gpr.py</code> is what runs during discovery; the <code>fname</code> module only loads on first use. This is why a registration-time error blocks the whole addon from appearing, but a runtime error in the implementation only surfaces when the user triggers it.
== The <code>.gpr.py</code> registration file ==
Every addon ships exactly one <code>.gpr.py</code> per folder, executed at startup by Gramps' plugin scanner. Its single job is to call <code>register(...)</code> one or more times, declaring the addon's ''metadata'' — what kind it is, what version of Gramps it targets, which implementation module to load on demand.
The general shape:
<syntaxhighlight lang="python">register(
GRAMPLET, # kind (see 04-addon-kinds)
id="HelloGramplet", # stable identifier — folder name
name=_("Hello Gramplet"), # user-visible label
description=_("A minimal example"),
version="1.0.0", # addon version, X.Y.Z
gramps_target_version="6.0", # which Gramps minor this targets
status=STABLE, # STABLE / BETA / EXPERIMENTAL / UNSTABLE
fname="hellogramplet.py", # implementation module
# kind-specific fields go here
gramplet="HelloGramplet",
gramplet_title=_("Hello"),
)</syntaxhighlight>
=== Fields every kind needs ===
{|
! Field
! Meaning
|-
| <code>id</code>
| Stable identifier; MUST match the folder name
|-
| <code>name</code>
| User-visible label, translatable
|-
| <code>version</code>
| Addon version, dotted <code>X.Y.Z</code>
|-
| <code>gramps_target_version</code>
| The Gramps minor this targets, e.g. <code>"6.0"</code>
|-
| <code>status</code>
| <code>STABLE</code>, <code>BETA</code>, <code>EXPERIMENTAL</code>, or <code>UNSTABLE</code>
|-
| <code>fname</code>
| The implementation module Gramps loads on first use
|}
=== Fields most kinds want ===
* <code>description</code> — shown in the Plugin Manager tooltip.
* <code>authors</code>, <code>authors_email</code> — credit and contact, both lists.
* <code>maintainers</code>, <code>maintainers_email</code> — only set if different from authors.
* <code>help_url</code> — wiki page name; Gramps prepends the base URL and may add a language extension. Don't wrap in <code>_()</code> unless you actually want per-language wiki pages.
* <code>audience</code> — <code>EVERYONE</code> (default), <code>EXPERT</code>, or <code>DEVELOPER</code>; filters visibility in the Plugin Manager. The constants live at <code>_pluginreg.py:75-77</code> — note <code>EVERYONE</code>, not <code>ALL</code> (an outdated wiki page documents <code>ALL</code>; the code has only ever used <code>EVERYONE</code>).
=== Kind-specific fields ===
Every kind adds its own. A few examples:
* <code>GRAMPLET</code> adds <code>gramplet</code> (class or function name), <code>gramplet_title</code>, <code>height</code>, <code>expand</code>, <code>navtypes</code>, <code>force_update</code>.
* <code>REPORT</code> adds <code>reportclass</code>, <code>optionclass</code>, <code>category</code>, <code>report_modes</code>, <code>require_active</code>.
* <code>TOOL</code> adds <code>toolclass</code>, <code>optionclass</code>, <code>category</code>, <code>tool_modes</code>.
* <code>QUICKVIEW</code> adds <code>runfunc</code>, <code>category</code>.
[[User:Eduralph/Sandbox/Gramps_6.0_Wiki_Manual_-_Addon_Development_-_Addon_Kinds|04-addon-kinds]] lists the kind-specific fields per kind. The authoritative reference is the <code>expand_*</code> helpers in [https://github.com/gramps-project/gramps/blob/maintenance/gramps60/gramps/gen/plug/_pluginreg.py <code>_pluginreg.py</code>].
=== Multiple registrations per file ===
A single <code>.gpr.py</code> may call <code>register(...)</code> more than once — for example a report that also exposes a quick view, or two related gramplets sharing one implementation module. Each call is independent metadata.
== Plugin discovery ==
Gramps walks the plugin path at startup, executes every <code>.gpr.py</code> it finds, and builds an in-memory catalog from each <code>register()</code> call. The implementation modules pointed to by <code>fname</code> are '''not''' loaded at this point — they're imported lazily on first invocation. This split matters for diagnostics:
* A <code>SyntaxError</code> or import failure in <code>.gpr.py</code> makes the addon disappear entirely from menus — the catalog never got an entry for it.
* A failure inside the implementation module surfaces only when the user triggers the addon, with a traceback in the Plugin Manager and the log window.
=== The plugin path ===
Plugin folders are searched under each path Gramps was configured to scan — typically the system-wide plugin dir plus the per-user plugin dir. The per-user dir is the safe one to develop in; system locations generally need elevated permissions and shouldn't be edited directly. The exact paths are platform-specific; [[6.0_Addons|the Addons page]] lists them.
=== Symlinks ===
Plugin discovery's symlink handling changed between 6.0 and 6.1:
* '''Gramps 6.0''' — symlinks are '''not''' followed. An addon symlinked in is invisible. Development loop: copy/<code>rsync</code> from working tree on save.
* '''Gramps 6.1+''' — symlinks '''are''' followed, with realpath-based dedup so cycles terminate. Symlinking the working tree into the user plugin dir works in place. (Gramps commit [https://github.com/gramps-project/gramps/commit/9443dcbb30 <code>9443dcbb30</code>] on <code>maintenance/gramps61</code>.) The symlink test is skipped on Windows because the platform's symlink behaviour is inconsistent without elevated privileges; on Windows, a physical copy remains the safe approach even on 6.1+.
Concrete sync recipes live in [[User:Eduralph/Sandbox/Gramps_6.0_Wiki_Manual_-_Addon_Development_-_Getting_Started|02-get-started]].
== Names Gramps injects into <code>.gpr.py</code> ==
The <code>.gpr.py</code> runs in a scope where several names are ''pre-populated'' by the plugin loader. You '''must not import''' them; Gramps puts them there and an <code>import</code> masks them with stale bindings.
{|
! Injected name
! Source
|-
| <code>register</code>
| the loader itself
|-
| <code>_</code> (and <code>ngettext</code>)
| the addon's local translation
|-
| Kind constants — <code>GRAMPLET</code>, <code>REPORT</code>, <code>TOOL</code>, …
| <code>gramps.gen.plug._pluginreg</code>
|-
| Status constants — <code>STABLE</code>, <code>BETA</code>, <code>EXPERIMENTAL</code>, <code>UNSTABLE</code>
| <code>_pluginreg.py:62-65</code>
|-
| Audience constants — <code>EVERYONE</code>, <code>EXPERT</code>, <code>DEVELOPER</code>
| <code>_pluginreg.py:75-77</code>
|-
| Report category constants — <code>CATEGORY_TEXT</code>, <code>CATEGORY_DRAW</code>, …
| <code>_pluginreg.py:141-149</code>
|-
| Tool category constants — <code>TOOL_DBPROC</code>, <code>TOOL_DBFIX</code>, …
| <code>_pluginreg.py:154-159</code>
|-
| Quick View category constants — <code>CATEGORY_QR_PERSON</code>, …
| <code>_pluginreg.py:163-174</code>
|-
| Report mode constants — <code>REPORT_MODE_GUI</code>, <code>REPORT_MODE_BKI</code>, …
| <code>_pluginreg.py</code>
|}
In the implementation module, none of these are injected — the rules are normal Python. Import what you need from <code>gramps.gen.*</code> there.
== Translation ==
Every user-visible string in the <code>.gpr.py</code> and in the implementation goes through <code>_()</code>. The function is set up differently in the two files because the <code>.gpr.py</code> runs in the injected-name scope.
'''In <code>.gpr.py</code>''': just use <code>_()</code>. The loader has already wired it.
<syntaxhighlight lang="python">register(
GRAMPLET,
id="HelloGramplet",
name=_("Hello"),
description=_("A minimal example"),
...
)</syntaxhighlight>
'''In the implementation module''': opt into the addon's own translation catalog at the top of the file, then use <code>_()</code> normally.
<syntaxhighlight lang="python">from gramps.gen.const import GRAMPS_LOCALE as glocale
_ = glocale.get_addon_translator(__file__).gettext</syntaxhighlight>
This binds <code>_</code> to translations stored in the addon's own <code>po/</code> folder rather than Gramps' core catalog. Without this line, <code>_()</code> falls back to the core catalog and your addon-specific strings stay in English regardless of UI language.
=== Plurals ===
Use <code>ngettext(singular, plural, n)</code> whenever a number is being formatted into a string. Languages with non-trivial plural rules (Russian, Polish, …) need both forms to render correctly.
<syntaxhighlight lang="python">msg = ngettext("{n} match", "{n} matches", n).format(n=n)</syntaxhighlight>
=== Disambiguating contexts ===
When the same English word translates differently in different contexts, add a context hint. Gramps' <code>_()</code> accepts <code>_(msg, context)</code>; the older <code>pgettext(context, msg)</code> form also works but the comma form is preferred because the source remains readable as plain English.
<syntaxhighlight lang="python">_("Source", "citation") # vs. _("Source", "person attribute")</syntaxhighlight>
== Logging ==
Use a module-level logger; never use <code>print()</code> for diagnostics.
<syntaxhighlight lang="python">import logging
LOG = logging.getLogger(".".join(__name__.split(".")[-2:]))
# or simply:
LOG = logging.getLogger(__name__)
LOG.debug("Reached the interesting branch with n=%d", n)
LOG.warning("Skipping malformed event %s", event.gramps_id)</syntaxhighlight>
Log output flows into:
* '''The Gramps log window''' (Help → Log) — visible to the user.
* '''stderr''' when Gramps is launched with <code>--debug</code> or with <code>GRAMPS_DEBUG=1</code> set.
See [[User:Eduralph/Sandbox/Gramps_6.0_Wiki_Manual_-_Addon_Development_-_Debug|09-debug]] for how to enable debug levels per logger.
== Lifecycle hooks ==
Every kind has its own entry points; the shape varies, but the pattern is consistent: a small number of named methods that Gramps calls at specific moments, and you override the ones you need.
=== Gramplets ===
Subclass <code>gramps.gen.plug.Gramplet</code>. The hooks Gramps calls:
{|
! Method
! When
|-
| <code>init(self)</code>
| Once, on first show. Build the UI here. Don't read the DB yet — it may not be open.
|-
| <code>db_changed(self)</code>
| When the active database changes. Reconnect any signals you wired on the old DB.
|-
| <code>active_changed(self, handle)</code>
| When the active person / family / etc. changes. Default is to call <code>update()</code>.
|-
| <code>main(self)</code>
| The work itself. May be a generator — <code>yield True</code> to keep going, <code>yield False</code> to stop.
|-
| <code>update(self)</code>
| Don't override. Calls <code>main()</code> for you; you call <code>update()</code> to schedule a redraw.
|-
| <code>on_load(self)</code> / <code>on_save(self)</code>
| When the gramplet's persistent data is loaded / saved.
|}
Inside the class, <code>self.dbstate.db</code> is your live database, <code>self.uistate</code> is the GUI state. See [[User:Eduralph/Sandbox/Gramps_6.0_Wiki_Manual_-_Addon_Development_-_Data_access|06-data-access]] for what you can do with <code>self.dbstate.db</code>.
=== Reports ===
Subclass <code>gramps.gen.plug.report.Report</code>. The constructor receives <code>(database, options_class, user)</code>. Override <code>write_report()</code> — that's the single hook Gramps calls. Everything else is plumbing you initialise in <code>__init__</code>.
=== Tools ===
Subclass from <code>gramps.gui.plug.tool</code>. The constructor receives <code>(dbstate, user, options_class, name, callback=None)</code> and does the work inline (there's no separate <code>run()</code> for non-CLI tools). For CLI mode, <code>tool_modes=[TOOL_MODE_CLI]</code> triggers a different entry path.
=== Quick Views ===
Plain function: <code>run(database, document, person_or_family_or_…)</code>. No class to subclass. Point <code>runfunc</code> at it in the registration.
=== Importers / Exporters ===
Plain function pointed to by <code>fname</code> + the kind's entry-point field. Signature varies by kind and minor; reading a live importer/exporter is the most reliable way to lock down the exact shape on your target branch.
== Signals: addons reacting to changes ==
Gramps' database and UI emit ''signals'' when state changes. Addons that need to stay in sync — gramplets that refresh on data changes, views that follow the selection — <code>connect()</code> to those signals.
=== The minimal pattern ===
<syntaxhighlight lang="python">key = self.dbstate.db.connect("person-update", self.cb_person_changed)
# … later, in teardown …
self.dbstate.db.disconnect(key)</syntaxhighlight>
<code>connect()</code> returns an opaque key; pass it to <code>disconnect()</code> when the addon shuts down or the database changes. Forgetting to disconnect leaves stale callbacks pointing into freed objects and crashes Gramps sooner or later.
=== The signals that matter most ===
{|
! Source
! Signal
! When
|-
| <code>dbstate.db</code>
| <code>person-add</code>, <code>family-add</code>, <code>event-add</code>, …
| One object added. Arg: list of handles.
|-
| <code>dbstate.db</code>
| <code>person-update</code>, <code>family-update</code>, …
| One object updated. Arg: list of handles.
|-
| <code>dbstate.db</code>
| <code>person-delete</code>, <code>family-delete</code>, …
| One object deleted. Arg: list of handles.
|-
| <code>dbstate.db</code>
| <code>person-rebuild</code>, <code>family-rebuild</code>, …
| Mass change (import, db repair). No args.
|-
| <code>dbstate.db</code>
| <code>home-person-changed</code>
| Home person changed. No args.
|-
| <code>dbstate</code>
| <code>database-changed</code>
| Active database swapped. Arg: the new db.
|-
| <code>dbstate</code>
| <code>no-database</code>
| No db is open.
|-
| <code>uistate</code>
| <code>nameformat-changed</code>, <code>filter-name-changed</code>, …
| Various UI preferences.
|-
| view's history
| <code>active-changed</code>
| Selected object changed. Arg: the new handle.
|}
Pattern: <code>person-update</code> / <code>family-update</code> / etc. fire one ''after'' a transaction commits, with a ''list'' of affected handles. They never fire mid-transaction, so callbacks can safely re-read the DB.
=== Subscribing to "anything changed" ===
A common gramplet pattern is "redraw on any structural change to the tree", typically done by wiring <code>db_changed</code>:
<syntaxhighlight lang="python">def db_changed(self):
self.dbstate.db.connect("person-add", self.update)
self.dbstate.db.connect("person-delete", self.update)
self.dbstate.db.connect("person-update", self.update)
self.dbstate.db.connect("family-add", self.update)
self.dbstate.db.connect("family-delete", self.update)
self.dbstate.db.connect("family-update", self.update)</syntaxhighlight>
For complex subscriptions across many object types, the <code>CallbackManager</code> in <code>gramps.gen.utils.callman</code> is a higher-level filter that lets you register dictionaries of <code>{signal: handler}</code> and tracks keys for <code>disconnect_all()</code> on teardown. See [[Signals_and_Callbacks|Signals and callbacks]] for the full inventory.
=== Signal ordering ===
Signals are deferred until a transaction commits and are emitted in a specific order: deletes first, then adds, then updates; within each phase, by object type in the order persons → families → sources → events → media → places → repositories → notes → tags → citations. This deterministic order matters when a single transaction touches related objects (a family merge deletes one family and updates another plus its members); a handler that re-reads the DB on <code>person-delete</code> will see a consistent state.
== Reading and writing the database ==
The DB API is covered in depth in [[User:Eduralph/Sandbox/Gramps_6.0_Wiki_Manual_-_Addon_Development_-_Data_access|06-data-access]]. The rule worth stating here, where every addon meets it:
<ul>
<li><p>'''Reading''' is unrestricted. Any addon may read freely from <code>self.dbstate.db</code>.</p></li>
<li><p>'''Writing''' goes through a transaction. Always:</p>
<syntaxhighlight lang="python">with DbTxn(_("Description for Undo history"), db) as trans:
person = db.get_person_from_handle(handle)
person.set_privacy(True)
db.commit_person(person, trans)</syntaxhighlight></li></ul>
The transaction message is user-visible in the Undo history; translate it.
== Declaring dependencies ==
Addons may need Python packages or system tools that aren't part of Gramps' core dependencies. Declare these in the registration so the plugin manager can surface a clear "missing X" message instead of a generic import failure.
=== <code>requires_mod</code> — Python modules ===
<syntaxhighlight lang="python">requires_mod = ["PIL", "lxml"]</syntaxhighlight>
Uses the '''importable''' module name (what you <code>import</code>), '''not''' the PyPI distribution name. PIL not Pillow, lxml fine either way (matches), yaml not PyYAML. Verify before you push:
<syntaxhighlight lang="python">from importlib.util import find_spec
assert find_spec("PIL") is not None</syntaxhighlight>
A mismatch shows up the first time the addon's tests run against a clean install — the import fails. Always verify the name with <code>find_spec</code> before publishing.
=== <code>requires_gi</code> — GObject Introspection bindings ===
<syntaxhighlight lang="python">requires_gi = [("GExiv2", "0.10")]</syntaxhighlight>
A list of <code>(namespace, version)</code> tuples. The user has to install these through their OS package manager; Gramps cannot install GI bindings. The version pin must match what your code actually imports — and on gramps61 the version handling for GExiv2 was rewritten (addons-source PR 829), so a <code>requires_gi</code> pinned for one branch isn't guaranteed correct on the other. Verify against the target branch's related code before assuming a cherry-pick is correct.
=== <code>requires_exe</code> — Executables on PATH ===
<syntaxhighlight lang="python">requires_exe = ["graphviz", "dot"]</syntaxhighlight>
External binaries the user must have installed. Gramps checks PATH for them and surfaces a missing-dependency message.
=== <code>depends_on</code> — Other addons ===
<syntaxhighlight lang="python">depends_on = ["libwebconnect"]</syntaxhighlight>
Other addons that must load first. The plugin manager resolves these automatically when the user installs your addon. Circular dependencies break the load and disable the addon — the loader chooses safety over guessing.
== Configuration and persistent settings ==
For settings that should survive between sessions, Gramps' configuration manager handles the file I/O and migration; you only declare the keys.
<syntaxhighlight lang="python">from gramps.gen.config import config as configman
config = configman.register_manager("my_addon")
config.register("section.key1", default_value)
config.register("section.key2", another_default)
config.load() # read existing settings file, if any
config.save() # write defaults out if the file didn't exist</syntaxhighlight>
<code>config.get("section.key1")</code> and <code>config.set("section.key1", value)</code> read and write at runtime. Gramplets persist via the lifecycle hook:
<syntaxhighlight lang="python">def on_save(self):
config.save()</syntaxhighlight>
The settings file lives in the addon's plugin folder by default. For a system-wide config (rare):
<syntaxhighlight lang="python">config = configman.register_manager("my_addon", use_config_path=True)</syntaxhighlight>
== See also ==
* [[User:Eduralph/Sandbox/Gramps_6.0_Wiki_Manual_-_Addon_Development_-_Getting_Started|02-get-started]] — the first end-to-end Gramplet putting these concepts together.
* [[User:Eduralph/Sandbox/Gramps_6.0_Wiki_Manual_-_Addon_Development_-_Addon_Kinds|04-addon-kinds]] — what each kind adds to the registration shape described here.
* [[User:Eduralph/Sandbox/Gramps_6.0_Wiki_Manual_-_Addon_Development_-_Data_access|06-data-access]] — the DB API surface.
* [[User:Eduralph/Sandbox/Gramps_6.0_Wiki_Manual_-_Addon_Development_-_API_Reference|07-api-reference]] — the curated <code>gramps.gen.*</code> surface that addons may import.
* [[User:Eduralph/Sandbox/Gramps_6.0_Wiki_Manual_-_Addon_Development_-_Troubleshoot|10-troubleshoot]] — what failure modes look like when one of these conventions is off.
* [[Signals_and_Callbacks|Signals and Callbacks]] — the standalone wiki page covering signals and the <code>CallbackManager</code> in more depth.
{{stub}}
Cross-cutting concerns every addon author hits regardless of kind.
Ordered by the sequence an author actually meets them, not
alphabetically. Authoritative cross-references inside the manual:
- per-kind specifics -> 04-addon-kinds
- DB API surface -> 06-data-access
- testing -> 08-testing
- failure modes -> 10-troubleshoot
-->
== Overview ==
The cross-cutting concerns every addon author hits regardless of which kind they're building. If something in a kind-specific page assumes a piece of background, it's described here.
[[File:plugin-discovery.svg|Fig. 1 — Plugin discovery and load sequence. Gramps scans the plugin directory at startup, executes each <code>register()</code> call into a metadata-only catalog, and loads the implementation module lazily when the user first invokes the addon.]]
Note that the catalog → invoke arrow is dashed: addon implementation modules are ''not'' loaded at startup. The <code>.gpr.py</code> is what runs during discovery; the <code>fname</code> module only loads on first use. This is why a registration-time error blocks the whole addon from appearing, but a runtime error in the implementation only surfaces when the user triggers it.
== The <code>.gpr.py</code> registration file ==
Every addon ships exactly one <code>.gpr.py</code> per folder, executed at startup by Gramps' plugin scanner. Its single job is to call <code>register(...)</code> one or more times, declaring the addon's ''metadata'' — what kind it is, what version of Gramps it targets, which implementation module to load on demand.
The general shape:
<syntaxhighlight lang="python">register(
GRAMPLET, # kind (see 04-addon-kinds)
id="HelloGramplet", # stable identifier — folder name
name=_("Hello Gramplet"), # user-visible label
description=_("A minimal example"),
version="1.0.0", # addon version, X.Y.Z
gramps_target_version="6.0", # which Gramps minor this targets
status=STABLE, # STABLE / BETA / EXPERIMENTAL / UNSTABLE
fname="hellogramplet.py", # implementation module
# kind-specific fields go here
gramplet="HelloGramplet",
gramplet_title=_("Hello"),
)</syntaxhighlight>
=== Fields every kind needs ===
{|
! Field
! Meaning
|-
| <code>id</code>
| Stable identifier; MUST match the folder name
|-
| <code>name</code>
| User-visible label, translatable
|-
| <code>version</code>
| Addon version, dotted <code>X.Y.Z</code>
|-
| <code>gramps_target_version</code>
| The Gramps minor this targets, e.g. <code>"6.0"</code>
|-
| <code>status</code>
| <code>STABLE</code>, <code>BETA</code>, <code>EXPERIMENTAL</code>, or <code>UNSTABLE</code>
|-
| <code>fname</code>
| The implementation module Gramps loads on first use
|}
=== Fields most kinds want ===
* <code>description</code> — shown in the Plugin Manager tooltip.
* <code>authors</code>, <code>authors_email</code> — credit and contact, both lists.
* <code>maintainers</code>, <code>maintainers_email</code> — only set if different from authors.
* <code>help_url</code> — wiki page name; Gramps prepends the base URL and may add a language extension. Don't wrap in <code>_()</code> unless you actually want per-language wiki pages.
* <code>audience</code> — <code>EVERYONE</code> (default), <code>EXPERT</code>, or <code>DEVELOPER</code>; filters visibility in the Plugin Manager. The constants live at <code>_pluginreg.py:75-77</code> — note <code>EVERYONE</code>, not <code>ALL</code> (an outdated wiki page documents <code>ALL</code>; the code has only ever used <code>EVERYONE</code>).
=== Kind-specific fields ===
Every kind adds its own. A few examples:
* <code>GRAMPLET</code> adds <code>gramplet</code> (class or function name), <code>gramplet_title</code>, <code>height</code>, <code>expand</code>, <code>navtypes</code>, <code>force_update</code>.
* <code>REPORT</code> adds <code>reportclass</code>, <code>optionclass</code>, <code>category</code>, <code>report_modes</code>, <code>require_active</code>.
* <code>TOOL</code> adds <code>toolclass</code>, <code>optionclass</code>, <code>category</code>, <code>tool_modes</code>.
* <code>QUICKVIEW</code> adds <code>runfunc</code>, <code>category</code>.
[[User:Eduralph/Sandbox/Gramps_6.0_Wiki_Manual_-_Addon_Development_-_Addon_Kinds|04-addon-kinds]] lists the kind-specific fields per kind. The authoritative reference is the <code>expand_*</code> helpers in [https://github.com/gramps-project/gramps/blob/maintenance/gramps60/gramps/gen/plug/_pluginreg.py <code>_pluginreg.py</code>].
=== Multiple registrations per file ===
A single <code>.gpr.py</code> may call <code>register(...)</code> more than once — for example a report that also exposes a quick view, or two related gramplets sharing one implementation module. Each call is independent metadata.
== Plugin discovery ==
Gramps walks the plugin path at startup, executes every <code>.gpr.py</code> it finds, and builds an in-memory catalog from each <code>register()</code> call. The implementation modules pointed to by <code>fname</code> are '''not''' loaded at this point — they're imported lazily on first invocation. This split matters for diagnostics:
* A <code>SyntaxError</code> or import failure in <code>.gpr.py</code> makes the addon disappear entirely from menus — the catalog never got an entry for it.
* A failure inside the implementation module surfaces only when the user triggers the addon, with a traceback in the Plugin Manager and the log window.
=== The plugin path ===
Plugin folders are searched under each path Gramps was configured to scan — typically the system-wide plugin dir plus the per-user plugin dir. The per-user dir is the safe one to develop in; system locations generally need elevated permissions and shouldn't be edited directly. The exact paths are platform-specific; [[6.0_Addons|the Addons page]] lists them.
=== Symlinks ===
Plugin discovery's symlink handling changed between 6.0 and 6.1:
* '''Gramps 6.0''' — symlinks are '''not''' followed. An addon symlinked in is invisible. Development loop: copy/<code>rsync</code> from working tree on save.
* '''Gramps 6.1+''' — symlinks '''are''' followed, with realpath-based dedup so cycles terminate. Symlinking the working tree into the user plugin dir works in place. (Gramps commit [https://github.com/gramps-project/gramps/commit/9443dcbb30 <code>9443dcbb30</code>] on <code>maintenance/gramps61</code>.) The symlink test is skipped on Windows because the platform's symlink behaviour is inconsistent without elevated privileges; on Windows, a physical copy remains the safe approach even on 6.1+.
Concrete sync recipes live in [[User:Eduralph/Sandbox/Gramps_6.0_Wiki_Manual_-_Addon_Development_-_Getting_Started|02-get-started]].
== Names Gramps injects into <code>.gpr.py</code> ==
The <code>.gpr.py</code> runs in a scope where several names are ''pre-populated'' by the plugin loader. You '''must not import''' them; Gramps puts them there and an <code>import</code> masks them with stale bindings.
{|
! Injected name
! Source
|-
| <code>register</code>
| the loader itself
|-
| <code>_</code> (and <code>ngettext</code>)
| the addon's local translation
|-
| Kind constants — <code>GRAMPLET</code>, <code>REPORT</code>, <code>TOOL</code>, …
| <code>gramps.gen.plug._pluginreg</code>
|-
| Status constants — <code>STABLE</code>, <code>BETA</code>, <code>EXPERIMENTAL</code>, <code>UNSTABLE</code>
| <code>_pluginreg.py:62-65</code>
|-
| Audience constants — <code>EVERYONE</code>, <code>EXPERT</code>, <code>DEVELOPER</code>
| <code>_pluginreg.py:75-77</code>
|-
| Report category constants — <code>CATEGORY_TEXT</code>, <code>CATEGORY_DRAW</code>, …
| <code>_pluginreg.py:141-149</code>
|-
| Tool category constants — <code>TOOL_DBPROC</code>, <code>TOOL_DBFIX</code>, …
| <code>_pluginreg.py:154-159</code>
|-
| Quick View category constants — <code>CATEGORY_QR_PERSON</code>, …
| <code>_pluginreg.py:163-174</code>
|-
| Report mode constants — <code>REPORT_MODE_GUI</code>, <code>REPORT_MODE_BKI</code>, …
| <code>_pluginreg.py</code>
|}
In the implementation module, none of these are injected — the rules are normal Python. Import what you need from <code>gramps.gen.*</code> there.
== Translation ==
Every user-visible string in the <code>.gpr.py</code> and in the implementation goes through <code>_()</code>. The function is set up differently in the two files because the <code>.gpr.py</code> runs in the injected-name scope.
'''In <code>.gpr.py</code>''': just use <code>_()</code>. The loader has already wired it.
<syntaxhighlight lang="python">register(
GRAMPLET,
id="HelloGramplet",
name=_("Hello"),
description=_("A minimal example"),
...
)</syntaxhighlight>
'''In the implementation module''': opt into the addon's own translation catalog at the top of the file, then use <code>_()</code> normally.
<syntaxhighlight lang="python">from gramps.gen.const import GRAMPS_LOCALE as glocale
_ = glocale.get_addon_translator(__file__).gettext</syntaxhighlight>
This binds <code>_</code> to translations stored in the addon's own <code>po/</code> folder rather than Gramps' core catalog. Without this line, <code>_()</code> falls back to the core catalog and your addon-specific strings stay in English regardless of UI language.
=== Plurals ===
Use <code>ngettext(singular, plural, n)</code> whenever a number is being formatted into a string. Languages with non-trivial plural rules (Russian, Polish, …) need both forms to render correctly.
<syntaxhighlight lang="python">msg = ngettext("{n} match", "{n} matches", n).format(n=n)</syntaxhighlight>
=== Disambiguating contexts ===
When the same English word translates differently in different contexts, add a context hint. Gramps' <code>_()</code> accepts <code>_(msg, context)</code>; the older <code>pgettext(context, msg)</code> form also works but the comma form is preferred because the source remains readable as plain English.
<syntaxhighlight lang="python">_("Source", "citation") # vs. _("Source", "person attribute")</syntaxhighlight>
== Logging ==
Use a module-level logger; never use <code>print()</code> for diagnostics.
<syntaxhighlight lang="python">import logging
LOG = logging.getLogger(".".join(__name__.split(".")[-2:]))
# or simply:
LOG = logging.getLogger(__name__)
LOG.debug("Reached the interesting branch with n=%d", n)
LOG.warning("Skipping malformed event %s", event.gramps_id)</syntaxhighlight>
Log output flows into:
* '''The Gramps log window''' (Help → Log) — visible to the user.
* '''stderr''' when Gramps is launched with <code>--debug</code> or with <code>GRAMPS_DEBUG=1</code> set.
See [[User:Eduralph/Sandbox/Gramps_6.0_Wiki_Manual_-_Addon_Development_-_Debug|09-debug]] for how to enable debug levels per logger.
== Lifecycle hooks ==
Every kind has its own entry points; the shape varies, but the pattern is consistent: a small number of named methods that Gramps calls at specific moments, and you override the ones you need.
=== Gramplets ===
Subclass <code>gramps.gen.plug.Gramplet</code>. The hooks Gramps calls:
{|
! Method
! When
|-
| <code>init(self)</code>
| Once, on first show. Build the UI here. Don't read the DB yet — it may not be open.
|-
| <code>db_changed(self)</code>
| When the active database changes. Reconnect any signals you wired on the old DB.
|-
| <code>active_changed(self, handle)</code>
| When the active person / family / etc. changes. Default is to call <code>update()</code>.
|-
| <code>main(self)</code>
| The work itself. May be a generator — <code>yield True</code> to keep going, <code>yield False</code> to stop.
|-
| <code>update(self)</code>
| Don't override. Calls <code>main()</code> for you; you call <code>update()</code> to schedule a redraw.
|-
| <code>on_load(self)</code> / <code>on_save(self)</code>
| When the gramplet's persistent data is loaded / saved.
|}
Inside the class, <code>self.dbstate.db</code> is your live database, <code>self.uistate</code> is the GUI state. See [[User:Eduralph/Sandbox/Gramps_6.0_Wiki_Manual_-_Addon_Development_-_Data_access|06-data-access]] for what you can do with <code>self.dbstate.db</code>.
=== Reports ===
Subclass <code>gramps.gen.plug.report.Report</code>. The constructor receives <code>(database, options_class, user)</code>. Override <code>write_report()</code> — that's the single hook Gramps calls. Everything else is plumbing you initialise in <code>__init__</code>.
=== Tools ===
Subclass from <code>gramps.gui.plug.tool</code>. The constructor receives <code>(dbstate, user, options_class, name, callback=None)</code> and does the work inline (there's no separate <code>run()</code> for non-CLI tools). For CLI mode, <code>tool_modes=[TOOL_MODE_CLI]</code> triggers a different entry path.
=== Quick Views ===
Plain function: <code>run(database, document, person_or_family_or_…)</code>. No class to subclass. Point <code>runfunc</code> at it in the registration.
=== Importers / Exporters ===
Plain function pointed to by <code>fname</code> + the kind's entry-point field. Signature varies by kind and minor; reading a live importer/exporter is the most reliable way to lock down the exact shape on your target branch.
== Signals: addons reacting to changes ==
Gramps' database and UI emit ''signals'' when state changes. Addons that need to stay in sync — gramplets that refresh on data changes, views that follow the selection — <code>connect()</code> to those signals.
=== The minimal pattern ===
<syntaxhighlight lang="python">key = self.dbstate.db.connect("person-update", self.cb_person_changed)
# … later, in teardown …
self.dbstate.db.disconnect(key)</syntaxhighlight>
<code>connect()</code> returns an opaque key; pass it to <code>disconnect()</code> when the addon shuts down or the database changes. Forgetting to disconnect leaves stale callbacks pointing into freed objects and crashes Gramps sooner or later.
=== The signals that matter most ===
{|
! Source
! Signal
! When
|-
| <code>dbstate.db</code>
| <code>person-add</code>, <code>family-add</code>, <code>event-add</code>, …
| One object added. Arg: list of handles.
|-
| <code>dbstate.db</code>
| <code>person-update</code>, <code>family-update</code>, …
| One object updated. Arg: list of handles.
|-
| <code>dbstate.db</code>
| <code>person-delete</code>, <code>family-delete</code>, …
| One object deleted. Arg: list of handles.
|-
| <code>dbstate.db</code>
| <code>person-rebuild</code>, <code>family-rebuild</code>, …
| Mass change (import, db repair). No args.
|-
| <code>dbstate.db</code>
| <code>home-person-changed</code>
| Home person changed. No args.
|-
| <code>dbstate</code>
| <code>database-changed</code>
| Active database swapped. Arg: the new db.
|-
| <code>dbstate</code>
| <code>no-database</code>
| No db is open.
|-
| <code>uistate</code>
| <code>nameformat-changed</code>, <code>filter-name-changed</code>, …
| Various UI preferences.
|-
| view's history
| <code>active-changed</code>
| Selected object changed. Arg: the new handle.
|}
Pattern: <code>person-update</code> / <code>family-update</code> / etc. fire one ''after'' a transaction commits, with a ''list'' of affected handles. They never fire mid-transaction, so callbacks can safely re-read the DB.
=== Subscribing to "anything changed" ===
A common gramplet pattern is "redraw on any structural change to the tree", typically done by wiring <code>db_changed</code>:
<syntaxhighlight lang="python">def db_changed(self):
self.dbstate.db.connect("person-add", self.update)
self.dbstate.db.connect("person-delete", self.update)
self.dbstate.db.connect("person-update", self.update)
self.dbstate.db.connect("family-add", self.update)
self.dbstate.db.connect("family-delete", self.update)
self.dbstate.db.connect("family-update", self.update)</syntaxhighlight>
For complex subscriptions across many object types, the <code>CallbackManager</code> in <code>gramps.gen.utils.callman</code> is a higher-level filter that lets you register dictionaries of <code>{signal: handler}</code> and tracks keys for <code>disconnect_all()</code> on teardown. See [[Signals_and_Callbacks|Signals and callbacks]] for the full inventory.
=== Signal ordering ===
Signals are deferred until a transaction commits and are emitted in a specific order: deletes first, then adds, then updates; within each phase, by object type in the order persons → families → sources → events → media → places → repositories → notes → tags → citations. This deterministic order matters when a single transaction touches related objects (a family merge deletes one family and updates another plus its members); a handler that re-reads the DB on <code>person-delete</code> will see a consistent state.
== Reading and writing the database ==
The DB API is covered in depth in [[User:Eduralph/Sandbox/Gramps_6.0_Wiki_Manual_-_Addon_Development_-_Data_access|06-data-access]]. The rule worth stating here, where every addon meets it:
<ul>
<li><p>'''Reading''' is unrestricted. Any addon may read freely from <code>self.dbstate.db</code>.</p></li>
<li><p>'''Writing''' goes through a transaction. Always:</p>
<syntaxhighlight lang="python">with DbTxn(_("Description for Undo history"), db) as trans:
person = db.get_person_from_handle(handle)
person.set_privacy(True)
db.commit_person(person, trans)</syntaxhighlight></li></ul>
The transaction message is user-visible in the Undo history; translate it.
== Declaring dependencies ==
Addons may need Python packages or system tools that aren't part of Gramps' core dependencies. Declare these in the registration so the plugin manager can surface a clear "missing X" message instead of a generic import failure.
=== <code>requires_mod</code> — Python modules ===
<syntaxhighlight lang="python">requires_mod = ["PIL", "lxml"]</syntaxhighlight>
Uses the '''importable''' module name (what you <code>import</code>), '''not''' the PyPI distribution name. PIL not Pillow, lxml fine either way (matches), yaml not PyYAML. Verify before you push:
<syntaxhighlight lang="python">from importlib.util import find_spec
assert find_spec("PIL") is not None</syntaxhighlight>
A mismatch shows up the first time the addon's tests run against a clean install — the import fails. Always verify the name with <code>find_spec</code> before publishing.
=== <code>requires_gi</code> — GObject Introspection bindings ===
<syntaxhighlight lang="python">requires_gi = [("GExiv2", "0.10")]</syntaxhighlight>
A list of <code>(namespace, version)</code> tuples. The user has to install these through their OS package manager; Gramps cannot install GI bindings. The version pin must match what your code actually imports — and on gramps61 the version handling for GExiv2 was rewritten (addons-source PR 829), so a <code>requires_gi</code> pinned for one branch isn't guaranteed correct on the other. Verify against the target branch's related code before assuming a cherry-pick is correct.
=== <code>requires_exe</code> — Executables on PATH ===
<syntaxhighlight lang="python">requires_exe = ["graphviz", "dot"]</syntaxhighlight>
External binaries the user must have installed. Gramps checks PATH for them and surfaces a missing-dependency message.
=== <code>depends_on</code> — Other addons ===
<syntaxhighlight lang="python">depends_on = ["libwebconnect"]</syntaxhighlight>
Other addons that must load first. The plugin manager resolves these automatically when the user installs your addon. Circular dependencies break the load and disable the addon — the loader chooses safety over guessing.
== Configuration and persistent settings ==
For settings that should survive between sessions, Gramps' configuration manager handles the file I/O and migration; you only declare the keys.
<syntaxhighlight lang="python">from gramps.gen.config import config as configman
config = configman.register_manager("my_addon")
config.register("section.key1", default_value)
config.register("section.key2", another_default)
config.load() # read existing settings file, if any
config.save() # write defaults out if the file didn't exist</syntaxhighlight>
<code>config.get("section.key1")</code> and <code>config.set("section.key1", value)</code> read and write at runtime. Gramplets persist via the lifecycle hook:
<syntaxhighlight lang="python">def on_save(self):
config.save()</syntaxhighlight>
The settings file lives in the addon's plugin folder by default. For a system-wide config (rare):
<syntaxhighlight lang="python">config = configman.register_manager("my_addon", use_config_path=True)</syntaxhighlight>
== See also ==
* [[User:Eduralph/Sandbox/Gramps_6.0_Wiki_Manual_-_Addon_Development_-_Getting_Started|02-get-started]] — the first end-to-end Gramplet putting these concepts together.
* [[User:Eduralph/Sandbox/Gramps_6.0_Wiki_Manual_-_Addon_Development_-_Addon_Kinds|04-addon-kinds]] — what each kind adds to the registration shape described here.
* [[User:Eduralph/Sandbox/Gramps_6.0_Wiki_Manual_-_Addon_Development_-_Data_access|06-data-access]] — the DB API surface.
* [[User:Eduralph/Sandbox/Gramps_6.0_Wiki_Manual_-_Addon_Development_-_API_Reference|07-api-reference]] — the curated <code>gramps.gen.*</code> surface that addons may import.
* [[User:Eduralph/Sandbox/Gramps_6.0_Wiki_Manual_-_Addon_Development_-_Troubleshoot|10-troubleshoot]] — what failure modes look like when one of these conventions is off.
* [[Signals_and_Callbacks|Signals and Callbacks]] — the standalone wiki page covering signals and the <code>CallbackManager</code> in more depth.
{{stub}}