From 9da4e35c9e4110a0f8a190d20956f927e7ab2601 Mon Sep 17 00:00:00 2001 From: "Eduard R." Date: Tue, 28 Jul 2026 01:15:28 +0200 Subject: [PATCH] docs(addon-development): Summary sections, verified corrections MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Regenerated from the docs-as-code source. Every page now opens with a "Summary" section that says what it covers, in what order, and when to come back to it, instead of a one-line stub. Corrections carried in the same pass, each checked against this repository and gramps on maintenance/gramps60: - 03-addon-kinds: 14 -> 16 registration kinds. REPORT(0)..CITE(15) in gramps/gen/plug/_pluginreg.py; QUICKREPORT is a deprecated alias of QUICKVIEW, not a separate kind. - 16-guidelines: addon-root __init__.py drops from MUST NOT to SHOULD NOT, with a MAY carve-out for addons that genuinely expose a package API. Seven addons here ship one today (DynamicWeb, ExcludeSubtreeFilter, GrampyScript, PlaceCleanup, PostgreSQLEnhanced, Query, Sqlite) and work. The previous rationale cited Mantis 12691, which is a different trap that __init__.py neither causes nor cures; the real cost is that the same module becomes reachable under two names between the runtime and test views. PluginManager.import_plugin does a top-level __import__, so the file never runs on the load path. - 04-fundamentals, 16-guidelines: get_addon_translator must be guarded — it raises ValueError when no compiled catalog exists and no configured language starts with en/C, so the unguarded form passes on an English-locale machine and crashes at import for the first user running a non-English UI. Adds the help_url rule (Addon:PageName, not a GitHub URL). - 09-troubleshoot: new entry for CustomFilters being None under test — reload_custom_filters() rebinds the module global, so importing the name first captures None permanently. - 12-packaging: LANGUAGE='en_US.UTF-8' removed from the make.py recipe (make.py on this branch never reads it); as-needed and manifest-check targets documented. - 07-testing: the tests/ package asymmetry re-argued on the grounds that actually hold. - 17-roadmap: opens the question of whether CI should flag an addon-root __init__.py — advisory at most, since a gate would start red on the seven addons that ship one. --- docs/addon-development/01-overview.md | 14 +++++- docs/addon-development/02-tutorials.md | 43 +++++++++++++++++-- docs/addon-development/03-addon-kinds.md | 8 +++- docs/addon-development/04-fundamentals.md | 24 ++++++++++- docs/addon-development/05-data-access.md | 8 +++- docs/addon-development/06-api-reference.md | 8 +++- docs/addon-development/07-testing.md | 10 ++++- docs/addon-development/08-debug.md | 8 +++- docs/addon-development/09-troubleshoot.md | 34 ++++++++++++++- docs/addon-development/10-code-analysis.md | 6 ++- .../11-internationalization.md | 6 ++- docs/addon-development/12-packaging.md | 23 +++++++--- docs/addon-development/13-community.md | 8 +++- docs/addon-development/14-compatibility.md | 8 +++- docs/addon-development/15-whats-new.md | 8 +++- docs/addon-development/16-guidelines.md | 32 +++++++++++--- docs/addon-development/17-roadmap.md | 32 +++++++++++--- 17 files changed, 237 insertions(+), 43 deletions(-) diff --git a/docs/addon-development/01-overview.md b/docs/addon-development/01-overview.md index 85a906284..3706f3c46 100644 --- a/docs/addon-development/01-overview.md +++ b/docs/addon-development/01-overview.md @@ -2,11 +2,21 @@ [Index](01-overview.md) · [Next →](02-tutorials.md) -## Overview +## Summary A Gramps **addon** extends the application without modifying core. You add a feature, ship it on your own schedule, and users install it from the in-app Plugin Manager — no fork of Gramps, no waiting on a core release to put new functionality in front of people. An addon is just a folder of Python on the plugin path, so the barrier to entry is low; the trade-off is that you build against Gramps' API and track it across versions. This is how most of Gramps' reports, tools, and gramplets are delivered, and the same door is open to you. -Addons are discovered from the plugin directory; see [the addon list](https://gramps-project.org/wiki/index.php/6.0_Addons) for what ships today. +**What you actually write.** Two files are the minimum: a registration file, `.gpr.py`, that declares the addon to Gramps, and an implementation module that Gramps imports the first time the addon is used. Optional `po/` and `tests/` directories sit alongside them. Gramps scans every `.gpr.py` at startup and builds its plugin catalog from the `register(...)` calls; implementation modules load **lazily**. There is no build step and no manifest beyond the registration file — [see below](#anatomy-of-an-addon). + +**What you can extend.** Gramps has no single "addon" shape: sixteen registration kinds each plug in at a different extension point — Gramplets, Views, Reports, Tools, Importers, Exporters, Quick Views, Docgens, Sidebars, Map services, Relationship calculators, filter Rules, Database backends, Thumbnailers, citation formatters, and a catch-all `GENERAL` kind for shared libraries. The kind you pick determines the registration fields, the base class you subclass, and where the addon surfaces in the UI. The full catalogue is [Addon Kinds](03-addon-kinds.md); [the addon list](https://gramps-project.org/wiki/index.php/6.0_Addons) shows what already ships. + +**What you build against.** An addon targets one Gramps minor version, declared as `gramps_target_version` in the registration — "6.0" means the 6.0 API on the `maintenance/gramps60` branch. Addons import from `gramps.gen.*`, the headless core; `gramps.gui.*` is available to GUI kinds but is a less stable surface. The API you may rely on is curated in [API Reference](06-api-reference.md), and what shifts between versions is in [Compatibility](14-compatibility.md) and [What's New](15-whats-new.md). + +**How it reaches users.** Three levels of ambition, and you can stop at any of them. Keep the addon in your own plugin directory and it is yours alone. Hand someone the folder and they drop it into theirs. Or submit it to the `addons-source` repository upstream, where `make.py` packages it into an `.addon.tgz` in the `addons` repository and refreshes the listing JSON that the in-app Plugin Manager fetches over HTTPS — at which point every Gramps user on that version can find and install it. That pipeline is [Packaging](12-packaging.md); what happens afterwards — the addon-list entry, the wiki page, the support duty — is [Community](13-community.md). + +**What is expected of you.** The bar is deliberately lower than for core, but two rules never soften: every new `.py` file carries a GPL-2.0-or-later license header with a copyright line, and every user-visible string is wrapped in `_()` so it can be translated. Beyond that: a test where the change is testable, code that passes the static checks, and — counter-intuitively — *not* touching the `version` field in a submission, since the release build manages it. All of it is written out, MUST by MUST, in [Rules](16-guidelines.md). + +**Who this is for.** Anyone writing or maintaining a Gramps addon — from a one-file Gramplet for personal use to a published addon with translations and a maintainer. If you are changing Gramps itself rather than extending it, you want the Core Development section instead. This page is the **start point** for the section: first a map to every other page, then everything a first-time author needs to go from "Gramps is installed" to "my addon shows up in the menu" — anatomy, prerequisites, and a minimal working Gramplet. The normative MUST / SHOULD rules every addon is held to live in [Rules](16-guidelines.md). diff --git a/docs/addon-development/02-tutorials.md b/docs/addon-development/02-tutorials.md index 60a69f3dc..bee0d0bd0 100644 --- a/docs/addon-development/02-tutorials.md +++ b/docs/addon-development/02-tutorials.md @@ -19,10 +19,14 @@ restart cycle) is not re-explained. --> -## Overview +## Summary End-to-end walkthroughs that take an author from empty folder to working addon. Each tutorial picks one kind, covers registration, implementation, and the reload cycle, and points at the conventions used to test it. +Five kinds are worked through here — Gramplet, Tool, Report, Quick View, and filter Rule. Between them they exercise most of the machinery an addon author meets: reading the database, subscribing to database signals, writing inside a transaction, driving the docgen abstraction that keeps a report independent of its output format, hooking into a context menu, and slotting a rule into the filter engine. Every tutorial is a working addon whose files are given whole rather than as elided fragments, so the code on the page is the code you can run. + +The five follow the same shape, so once you have read one you can skim the rest: **Layout** (the files and the folder), the **`.gpr.py`** registration in full with its kind-specific fields called out, the **implementation module** in full, **What's new** (what this tutorial adds that the earlier ones didn't), and **Try it** (how to load it and confirm it works). What the tutorials deliberately leave out — license headers and type hints, both required in shipped addons — is spelled out in [A note on tutorial-style code](#a-note-on-tutorial-style-code) below. + Read these in order or skip to the one that matches what you're building — they're independent. They assume you've already followed [the getting-started walkthrough in 01-overview](01-overview.md#your-first-addon-a-minimal-gramplet), so we don't re-explain the user plugin directory or the restart cycle. | Tutorial | Kind | What it shows | @@ -37,12 +41,13 @@ For the conceptual map, see [01-overview](01-overview.md). For the full inventor ### A note on tutorial-style code -The implementation modules below show the smallest code that demonstrates each kind. Two things are deliberately omitted to keep the lesson in focus, and both are **required** for shipped addons: +The implementation modules below show the smallest code that demonstrates each kind. Three things are deliberately omitted to keep the lesson in focus, and all three belong in a shipped addon: - A **GPL-2.0-or-later license header** at the top of every `.py` file. Copy the header from any existing addon, or see [16-guidelines → Coding style](16-guidelines.md#coding-style). - **Type hints** on public functions and methods (Python 3.10+ syntax — `X | None`, `list[X]`). The tutorials skip them for readability; production addons should include them per [16-guidelines → Coding style](16-guidelines.md#coding-style). +- A **guarded translator binding**. Every module below binds `_` with the bare one-liner `_ = glocale.get_addon_translator(__file__).gettext`. That call raises `ValueError` for a user whose language list has no `en`/`C` entry when the addon has no compiled `locale/` — which is exactly the state these tutorial addons are in. Shipped addons wrap it in `try` / `except ValueError` and fall back to `glocale.translation`; see [04-fundamentals → Translation](04-fundamentals.md#translation). -Both are CI-checked on gramps core PRs (Black formats around the license header; `mypy` verifies the type hints); addons-source doesn't gate on them today but the rules apply to addon code regardless. +The first two are CI-checked on gramps core PRs (Black formats around the license header; `mypy` verifies the type hints); addons-source doesn't gate on any of the three today, but the rules apply to addon code regardless. ## A live Gramplet @@ -50,6 +55,12 @@ Both are CI-checked on gramps core PRs (Black formats around the license header; The Hello Gramplet from [the overview's walkthrough](01-overview.md#your-first-addon-a-minimal-gramplet) was static text. This one is dynamic — it subscribes to signals and re-reads the DB on each update. +**What the user sees.** A panel in the sidebar showing the active person's Gramps ID in bold, then one line per event recorded directly against that person — the event type and its date. Click a different person in the People view and the panel redraws for them; edit one of their events in another window and the panel picks the change up on its own, with no refresh button to press. + +**What it teaches.** Three mechanisms nearly every non-trivial Gramplet needs. Subscribing to database signals through `self.connect()` rather than `self.dbstate.db.connect()`, so Gramps tracks the subscriptions and tears them down when the Gramplet closes or the tree is swapped — the forgotten-disconnect bug class simply doesn't arise. Overriding `active_changed()` so the widget follows the user's navigation instead of showing whatever was current when it loaded. And dereferencing event *refs* into real Event objects, which is the handle-following pattern the whole database API is built on ([Data access](05-data-access.md)). Underneath all three sits the Gramplet lifecycle split: `init()` runs once to build the static parts, `main()` runs on every refresh. + +**Why this example.** Direct events only — nothing inherited from the person's families, no sorting, no filtering — keeps the module short enough to read in one pass. Each of those omissions is a natural next exercise: family events come from the person's family handles, and sorting by date means comparing `Date` objects rather than the strings they render to. + ### Layout Two files in a new folder `PersonEvents/`: @@ -150,6 +161,12 @@ For the API surface this tutorial used (handles, refs, `iter_*`, `commit_*`), se Tools differ from gramplets in two ways: they're invoked from the Tools menu (not always visible), and they always carry an Options class — even a tool with no options must register an empty `ToolOptions` subclass. +**What the user sees.** An entry under *Tools → Analysis and Exploration*. Choosing it walks every person in the tree and puts up a modal dialog listing the Gramps ID and name of each one whose birth date is unknown — whether because no birth event is attached at all, or because a birth event exists but carries an empty date. If every person does have a date, the dialog says so rather than showing an empty list: a clean result still deserves an answer. + +**What it teaches.** The Tool / ToolOptions pair, and the fact that a GUI tool does its work *in `__init__`* — there is no separate `run()` to override, which surprises most people once. It also shows a whole-database scan done the cheap way, iterating with `db.iter_people()` instead of materialising a list, and the two-step check that separates "no birth event" from "birth event with no date" — a distinction that quietly breaks data-quality reports that only test for the first. + +**Why this example.** A data-quality scan is the archetypal Tool: read everything, decide something, report back. This one stays deliberately read-only, with the write path split out into a separate [Writing data](#writing-data) section below, so the `DbTxn` rule every mutating tool must follow is visible on its own rather than tangled into the example. + ### Layout ``` @@ -260,7 +277,7 @@ After restart, the tool appears in *Tools → Analysis and Exploration → Missi ## A text Report -**Goal.** A simple text report that summarises the database — number of people, number of families, count by gender. Produces the same content through PDF, HTML, ODF, or any other docgen-supported format. +**Goal.** A simple text report that summarises the database — total people, counts by gender, and surname statistics. Produces the same content through PDF, HTML, ODF, or any other docgen-supported format. Reports are the heaviest of the everyday addon kinds. Three pieces work together: @@ -268,6 +285,12 @@ Reports are the heaviest of the everyday addon kinds. Three pieces work together - An **Options** class that defines user-adjustable options and the paragraph / font styles. - A **registration** call wiring both into the menu. +**What the user sees.** An entry under *Reports → Text Reports*. Choosing it opens the standard report dialog — output format, target file, and the one option this report contributes — and produces a short document: a title, the total number of people, the counts by gender, how many distinct surnames the tree holds, and which surname is the most common with its count. Run the same report as PDF, as ODF, and as plain text and you get the same content three times, each rendered in that format's idiom. + +**What it teaches.** The three-part structure above, and the one idea that sets reports apart from every other addon kind: you never write output. You make `start_paragraph(style)` / `write_text()` / `end_paragraph()` calls against `self.doc`, and whichever document generator the user picked turns them into a PDF, an ODF file, or HTML. That is why the style names (`DBS-Title`, `DBS-Normal`) are defined in the Options class rather than the report body — fonts and layout are the Options class's business, content is the Report's. The example also carries the standard localisation option, which is the reason for `self._()` alongside the module-level `_()`: report *output* follows the locale the user chose for this run, which need not be the locale the interface is running in. + +**Why this example.** Counting is the least interesting thing a report can do, and that is the point — with the data-walking reduced to a couple of `Counter` tallies, what remains on the page is the report scaffolding itself. Swap `_count()` for something substantial and everything around it stays exactly as written. + ### Layout ``` @@ -427,6 +450,12 @@ For more on the docgen abstraction, see [Report Generation](https://gramps-proje Quick Views are the shortest path to a usable report. There's no class to subclass and no options form to maintain — just a `run()` function and the registration. They're written against the **Simple Access API** (`SimpleAccess`, `SimpleDoc`), which trades some power for very little code. +**What the user sees.** A *Quick View → Siblings* item on the right-click menu of any person, in the People view or the person editor. Choosing it opens a small window headed with that person's name and a three-column table — person, gender, birth date — of their brothers and sisters, gathered from every family in which they appear as a child, so half-siblings from a parent's second family are included. The rows are live: clicking one navigates to that person. + +**What it teaches.** The Simple Access layer, which is the reason the whole addon is one short function. `sdb.child_in()`, `sdb.children()`, `sdb.name()`, and `sdb.birth_date()` each hide a handle dereference and a formatting decision you would otherwise write out by hand. Around that: the fixed `run(database, document, person)` signature the QuickView kind imposes, `QuickTable` for output whose rows link back to real Gramps objects, and `document.has_data` — the flag that lets Gramps tell "this person has no siblings" apart from "the Quick View produced nothing". + +**Why this example.** Siblings aren't stored anywhere. Nothing in the database records them; they have to be derived by going up to each family the person is a child in and back down to its other children — which is the shape of most genealogical questions worth asking. It also has one small correctness detail of the kind real addons are full of: the person themselves appears in that child list and has to be skipped. + ### Layout ``` @@ -506,6 +535,12 @@ For Quick Views that don't fit the Simple Access surface, you can reach for the Filter rules are the smallest addon kind by line count and the one with the most reuse: a single rule, written once, drops into every filter the user composes — search, narrative website, reports, gramplets that accept a filter. +**What the user sees.** A new entry, *People with at least N children*, in the *Family filters* group of *Edit → Person Filter Editor → Add → Add Rule*, with a single input box labelled "Minimum count:". Added to a filter, it is indistinguishable from a built-in rule — it combines with other rules in the same filter, and travels everywhere that filter is used. + +**What it teaches.** How declarative a rule is. `labels` declares the inputs and produces the dialog's text boxes; the values come back on `self.list` as raw strings straight from the GUI, which is why the example parses them defensively rather than trusting the input. `name`, `category`, and `description` are read directly off the class — no instance exists yet when Gramps builds the Add Rule dialog. All the actual work happens in `apply_to_one()`, called once per person, returning `True` or `False`. The counting short-circuits: once the running total reaches the minimum, it stops walking families rather than finishing the count it no longer needs. + +**Why this example.** It shows the best effort-to-reach ratio in the whole plugin system. One small class gets you a rule that every filter-aware part of Gramps can use — search, reports, the narrative website, filter gramplets — with no integration work in any of them, because the filter framework is uniform. The [Optional hooks](#optional-hooks) section then covers `prepare()` and `reset()`, for rules whose per-object work is expensive enough to be worth precomputing once instead of repeating for every person in the tree. + ### Layout ``` diff --git a/docs/addon-development/03-addon-kinds.md b/docs/addon-development/03-addon-kinds.md index 9abcfa056..39b40616b 100644 --- a/docs/addon-development/03-addon-kinds.md +++ b/docs/addon-development/03-addon-kinds.md @@ -15,9 +15,13 @@ The numeric values aren't author-facing; cite by name. --> -## Overview +## Summary -Gramps doesn't have one "addon" shape — it has 14 of them, each registered with a different `register(KIND, …)` constant and each plugged in at a different extension point. This page is the index over all of them, with the registration constant, the UI location, the base class to subclass, and a pointer onward. Use it to answer the first question every prospective addon author asks: **which kind of thing am I writing?** +Gramps doesn't have one "addon" shape — it has 16 of them, each registered with a different `register(KIND, …)` constant and each plugged in at a different extension point. This page is the index over all of them, with the registration constant, the UI location, the base class to subclass, and a pointer onward. Use it to answer the first question every prospective addon author asks: **which kind of thing am I writing?** + +The page opens with a **kinds-at-a-glance table** — every constant, where it surfaces in the UI, and what it is typically used for — so you can narrow to one or two candidates in a single read. **Per-kind notes** then take each in turn: the fields the registration adds beyond the common set, the base class to subclass (where the kind has one), the minimum viable shape of the implementation module, and a link onward to the tutorial or the live addon worth reading as an example. The kinds an author is most likely to write get a section of their own; the rarer ones are covered in a paragraph each. A closing note covers the case where one addon folder registers **several kinds at once** — a Tool plus the Gramplet that displays its output, say — which is common and entirely supported. + +Choosing wrong is cheap to correct early and expensive later: the kind determines the base class, the entry-point signature, and where the user finds the addon, so it is worth a few minutes here before writing code. If two kinds both look plausible, the tie-breaker is usually *how the user reaches it* — a menu item, a dashboard widget, or a right-click on an object. Source of truth for the constants: [`gramps/gen/plug/_pluginreg.py`](https://github.com/gramps-project/gramps/blob/maintenance/gramps60/gramps/gen/plug/_pluginreg.py). diff --git a/docs/addon-development/04-fundamentals.md b/docs/addon-development/04-fundamentals.md index 74639de4f..b4492c6f9 100644 --- a/docs/addon-development/04-fundamentals.md +++ b/docs/addon-development/04-fundamentals.md @@ -12,10 +12,16 @@ - failure modes -> 09-troubleshoot --> -## Overview +## Summary 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. +This is the longest page in the section and the one to come back to rather than read once. It works outward from the registration file: **`.gpr.py`** field by field — the fields every kind needs, the ones most kinds want, the kind-specific ones, and how to put several registrations in one file — then **plugin discovery**: what Gramps scans, what the plugin path is, and the symlink rule that differs between 6.0 and 6.1. From there it covers the **names Gramps injects** into a `.gpr.py` (`register`, `_`, the kind constants — available without import, and *only* in that file) and the wider **provided environment** the startup sequence hands an addon. + +The rest is the runtime side of addon authoring: **translation** (`_()`, plurals via `ngettext`, disambiguating contexts), **logging** through module-level loggers rather than `print`, the **lifecycle hooks** each kind is expected to override, **signals** for addons that must react to database changes — the minimal subscribe pattern, which signals matter, how to catch "anything changed", and why ordering can surprise you — a pointer into database **reading and writing**, the four **dependency declarations** (`requires_mod`, `requires_gi`, `requires_exe`, `depends_on`) and how each fails when unmet, and finally **configuration and persistent settings** for addons that need to remember something between sessions. + +You do not need all of this before your first addon; [Addon Development](01-overview.md) and [Tutorials](02-tutorials.md) get you running. Come here when something behaves unexpectedly, or when an addon grows past the single-file stage. + ![Fig. 1 — Plugin discovery and load sequence. Gramps scans the plugin directory at startup, executes each `register()` call into a metadata-only catalog, and loads the implementation module lazily when the user first invokes the addon.](_media/plugin-discovery.svg) Note that the catalog → invoke arrow is dashed: addon implementation modules are *not* loaded at startup. The `.gpr.py` is what runs during discovery; the `fname` 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. @@ -58,7 +64,7 @@ register( - `description` — shown in the Plugin Manager tooltip. - `authors`, `authors_email` — credit and contact, both lists. - `maintainers`, `maintainers_email` — only set if different from authors. -- `help_url` — wiki page name; Gramps prepends the base URL and may add a language extension. Don't wrap in `_()` unless you actually want per-language wiki pages. +- `help_url` — wiki page name; Gramps prepends the base URL and may add a language extension. Give the addon's own Gramps wiki page in the `Addon:PageName` form — **not** a GitHub URL (upstream `addons-source/AGENTS.md` → Plugin Registration). Don't wrap in `_()` unless you actually want per-language wiki pages. - `audience` — `EVERYONE` (default), `EXPERT`, or `DEVELOPER`; filters visibility in the Plugin Manager. The constants live at `_pluginreg.py:75-77` — note `EVERYONE`, not `ALL` (an outdated wiki page documents `ALL`; the code has only ever used `EVERYONE`). ### Kind-specific fields @@ -156,6 +162,20 @@ _ = glocale.get_addon_translator(__file__).gettext This binds `_` to translations stored in the addon's own `po/` folder rather than Gramps' core catalog. Without this line, `_()` falls back to the core catalog and your addon-specific strings stay in English regardless of UI language. +**Guard it for the no-catalog case.** `get_addon_translator` raises `ValueError` when the addon has no compiled `locale/` yet *and* the user's language list contains nothing starting with `en` or `C` (`gramps/gen/utils/grampslocale.py:536-540`). On an English-locale machine you will never hit it; the first user with a non-English UI gets an import-time crash instead of an untranslated addon. Fall back to the core translator: + +```python +from gramps.gen.const import GRAMPS_LOCALE as glocale + +try: + _trans = glocale.get_addon_translator(__file__) +except ValueError: + _trans = glocale.translation +_ = _trans.gettext +``` + +This is the form upstream `addons-source/AGENTS.md` prescribes, and it is what a shipped addon should use — the shorter one-liner above is fine while experimenting, and is what the tutorials use to keep the lesson in focus. + ### Plurals Use `ngettext(singular, plural, n)` whenever a number is being formatted into a string. Languages with non-trivial plural rules (Russian, Polish, …) need both forms to render correctly. diff --git a/docs/addon-development/05-data-access.md b/docs/addon-development/05-data-access.md index 3697c9439..8f7b49c3c 100644 --- a/docs/addon-development/05-data-access.md +++ b/docs/addon-development/05-data-access.md @@ -15,7 +15,7 @@ actually need. --> -## Overview +## Summary Every addon that does anything useful with a family tree reads or writes through the **database API** — the `DbReadBase` / `DbWriteBase` interface implemented by Gramps' database backends (BSDDB historically, SQLite from 6.0 onward). @@ -27,6 +27,12 @@ db = dbstate.db # this is your entry point The same `db` works for read-only addons (reports, gramplets, quick views) and for tools that mutate data. Mutation goes through transactions; see [Mutating data](#mutating-data) below. +The page follows the order in which questions actually come up. It starts with the distinction that trips up nearly everyone — **handles versus Gramps IDs**: the handle is the immutable internal key you traverse the data with, the `gramps_id` (`I0042`, `P0001`) is the user-facing label that users can and do change, and mixing them up produces code that works on your tree and fails on someone else's. Then **reading one object at a time** by handle or ID, and **iterating whole object types** with the cursor and iterator methods that keep memory flat on large trees. + +From there it gets into structure: **following references** from one object to another (a Person to their Families, a Family to its Events, an Event to its Place), and **backlinks** — the reverse question, "what refers to this object?", which is how you find every citation of a source or every event at a place. **Filters** covers reusing Gramps' own filter machinery instead of hand-rolling selection logic. **Mutating data** covers the write path: every change goes inside a `DbTxn` transaction, which is what makes it undoable in the UI and atomic on failure. + +Two shorter sections close it out: **testing data access** — how to exercise this code without a GUI, cross-referenced to [Testing](07-testing.md) — and **performance notes** on the access patterns that stay fast as trees grow into the tens of thousands of objects. + ## Identifying objects: handles vs Gramps IDs Every primary object (Person, Family, Event, Place, Source, Citation, Repository, Media, Note, Tag) has **two identifiers**: diff --git a/docs/addon-development/06-api-reference.md b/docs/addon-development/06-api-reference.md index fec5d9b04..797f453e7 100644 --- a/docs/addon-development/06-api-reference.md +++ b/docs/addon-development/06-api-reference.md @@ -16,11 +16,15 @@ - report-generation.md (88L) — Text / Draw / Graphviz / Web categorisation --> -## Overview +## Summary The curated `gramps.gen.*` surface addons are allowed to import. `gen` is the self-contained core submodule (it must not import from `gui` or `plugins`); importing only from `gen` keeps an addon portable across UI variants and testable without a display. -This page is a navigator, not a generated API dump. For exhaustive signatures, read the source of the module referenced — the [upstream Sphinx docs](https://gramps-project.org/docs/) carry the same information formatted for browsing. +This page is a navigator, not a generated API dump. For exhaustive signatures, read the source of the module referenced — the [upstream Sphinx docs](https://gramps-project.org/docs/) carry the same information formatted for browsing. What it gives you instead is the shape of the surface: which module to reach for, what it is for, and which of the many things in `gen` an addon should actually depend on. + +**Allowed surface** groups the API by the job you are doing. *Database* is the `DbReadBase` / `DbWriteBase` entry point and its lookup and iteration methods. *Object model* is the primary and secondary object classes — Person, Family, Event, Place and the rest, plus the Name, Date, and Attribute types they contain. *Types and IDs*, *Errors*, and *Plugin base classes* cover the enumerations, the exception hierarchy worth catching, and the classes each addon kind subclasses. Report authors get three sections of their own: *Report categories*, the *Document API* — the docgen calls that let one report render to PDF, ODF, or HTML without knowing which — and *Paper geometry* for the Draw and Tree categories that lay out on a physical page. The rest covers *Locale and translation*, *Filters and selection*, *Logging*, and the *Simple Access* convenience layer that Quick Views are written against. + +**What's NOT API** is the section that saves the most rework. `gramps.gui.*` and `gramps.plugins.*` are internal to the distribution: importing from them is what makes an addon break on the next Gramps release. A few GUI kinds unavoidably touch `gui`, and those exceptions are named — but the guidance is to keep logic behind a `gen`-only boundary so it stays testable without a display, and to ask upstream to promote anything genuinely missing into `gen` rather than reaching around it. ## Allowed surface diff --git a/docs/addon-development/07-testing.md b/docs/addon-development/07-testing.md index 907eb69ef..649c9f8c8 100644 --- a/docs/addon-development/07-testing.md +++ b/docs/addon-development/07-testing.md @@ -16,12 +16,18 @@ these tests catch. --> -## Overview +## Summary How to test an addon without launching the GUI on every iteration — the test framework, the layout conventions, the fixtures that work, and the platform-aware rules that keep tests portable across Linux, Windows, and Mac. A working test suite is what makes an addon **maintainable across Gramps releases**. The matrix of (Gramps version × OS) makes manual testing impossible at scale; the per-OS prefix conventions below let a single CI matrix verify your addon against every supported combination automatically. +The page starts with the ground rules: the framework is **stdlib `unittest`** and nothing else — no pytest, no third-party runner — plus the class-header convention and the `tests/` **layout** each addon follows, including why the per-addon `tests/__init__.py` exists and why it must stay empty. Two sections then cover the mechanics that are easy to get wrong: the **GTK-pin contract** (Gramps pins the GObject-introspection versions once at startup, so an addon module must never call `gi.require_version` itself — the pin belongs in the addons-source repo-root `tests/__init__.py`, and copying it into a module produces the works-in-tests, breaks-in-Gramps failure) and **loading by dotted path** rather than by filesystem path or `unittest discover`, which is what keeps the namespace-package semantics intact. + +The middle of the page is about writing tests that hold up. **Mocked versus `example.gramps`-backed** weighs the two fixture styles and says when each is the right call — a mock is fast and precise, the shipped example tree is realistic and catches assumptions a mock would rubber-stamp. **Tests must run without `requires_mod` dependencies** covers keeping a suite green on a machine that lacks your addon's optional imports, by mocking at the import boundary or skipping cleanly. **What to test** and **what the test catches that the GUI doesn't** are the judgement calls: what is worth asserting, and why a passing manual click-through is weaker evidence than it feels. The closing section is the concrete commands for **running tests locally**, per addon and for the whole suite. + +One convention on the page belongs to CI rather than to your own runs: the **filename prefixes**. Test files are `test_*.py`, and the `test_linux_*`, `test_windows_*`, and `test_integration_*` variants scope a file to where it can actually run — which is what lets one CI matrix cover both Linux and Windows without either runner failing on tests that were never meant for it. + ## Framework: stdlib `unittest` Use stdlib `unittest`. Don't use pytest. @@ -78,7 +84,7 @@ The marker is **hygiene, not a bug fix**. Python 3.3+'s implicit namespace packa The convention crystallises as: every addon's `tests/` **should** have an `__init__.py`; the addon directory itself **should not**. -The asymmetry matters. The addon directory must remain a plain namespace dir — Gramps' plugin loader puts the addon dir on `sys.path` and imports `.py` by name. Making the addon dir a regular package can disturb plugin loading (and the [Mantis 12691](https://gramps-project.org/bugs/view.php?id=12691) namespace trap lives in exactly this area). The `tests/` subfolder has no such constraint, so making it an explicit package is free. +The asymmetry matters, though not for the reason it is often given. Gramps' plugin loader puts the addon dir itself on `sys.path` and imports the `fname` module as a *top-level* name, so an addon-root `__init__.py` never runs at load time and does not break the addon — seven addons on `maintenance/gramps60` ship one and work. What it does is make the same file importable under two names (top-level at runtime, `.` from the repo root under test), which yields two module objects and two copies of everything in them. Keeping the addon root a plain directory keeps the two views identical; the `tests/` subfolder has no such concern, so making it an explicit package is free. See [16-guidelines → Structure](16-guidelines.md#structure) for the full rule and the one legitimate exception. This is what [addons-source PR 930](https://github.com/gramps-project/addons-source/pull/930) (Gary Griffin) is moving toward. diff --git a/docs/addon-development/08-debug.md b/docs/addon-development/08-debug.md index b12f46970..194195651 100644 --- a/docs/addon-development/08-debug.md +++ b/docs/addon-development/08-debug.md @@ -11,12 +11,18 @@ PrerequisitesCheckerGramplet, the Mac no-debug-mode caveat). --> -## Overview +## Summary How to see what an addon is actually doing — where it logs, how to enable verbose output, and the patterns for reproducing a problem without sitting through a full Gramps launch cycle each time. Most addon bugs are reachable through three escalating tools, in order: read the log window, enable per-logger debug output, or write a tight repro script that bypasses the GUI entirely. The heavier tools (pdb, gdb) are documented at the bottom for the cases where the lighter ones don't suffice. +The page works up that ladder. **Where addon output goes** establishes the two surfaces every logging call feeds — the in-app log window under *Help → Log*, always populated, and stderr, populated only when Gramps was launched from a terminal — and why `print()` reaches neither. **Default log levels** explains that Gramps runs the root logger at `WARNING`, so your `DEBUG` and `INFO` lines are invisible until you lower the bar, either with the `--debug=` command-line flag or with a module-level override kept strictly to development. **Reproduction scripts that bypass the GUI** is the technique that saves the most time: driving your addon's logic directly against a database, with no launch cycle and no clicking, so the edit-run-observe loop takes seconds. **`PrerequisitesCheckerGramplet`** is the in-app diagnostic for the class of problem that turns out to be a missing or wrong-versioned dependency rather than a bug in your code. + +**Platform notes** matter more than they look: the three platforms are not equivalent for debugging. Linux behaves as documented; on Windows the launcher usually has no terminal attached, so you launch from MSYS2 UCRT64 to see stderr; and macOS has no debug-mode equivalent at all, which is why some Mac problems can only be triaged through the reporter's screenshots. **Heavier tools** closes the page with `pdb`, `python -m trace`, `gdb` for the C layer under PyGObject, and profiling for problems that are about speed rather than correctness. + +If your symptom is already a recognisable one, go to [Troubleshoot](09-troubleshoot.md) first — it indexes the common failures by what you see. This page is the technique reference behind it. + ## Where addon output goes Two surfaces, both populated by the same logging calls: diff --git a/docs/addon-development/09-troubleshoot.md b/docs/addon-development/09-troubleshoot.md index 19bb7d9a0..123445579 100644 --- a/docs/addon-development/09-troubleshoot.md +++ b/docs/addon-development/09-troubleshoot.md @@ -13,11 +13,15 @@ trace), this one is symptom-first. --> -## Overview +## Summary The failure modes that bite first-time addon authors, organised by symptom. Each entry is "what you see → why → what to do." Read this sideways: jump to the symptom that matches what you're seeing, follow the link out to the relevant chapter for the fix in depth. -For technique-level coverage (pdb, gdb, profilers), see [08-debug](08-debug.md). For the normative rules an addon must satisfy, see [16-guidelines](16-guidelines.md). +The entries are grouped into six families, roughly in the order an addon meets them. **Loading and discovery** — the addon doesn't appear anywhere, edits vanish on restart, or the folder is present but ignored — is where most first-day problems land, and the cause is usually the plugin path, a `.gpr.py` error swallowing the whole addon, or the symlink rule. **Imports and Python namespace traps** covers the ones that look like Python bugs rather than Gramps bugs: `from import ` binding the submodule instead of the class, `requires_mod` naming the PyPI distribution instead of the importable module, and a `requires_gi` declaration that is valid on 6.0 and broken on 6.1. + +**Database access** covers the errors that only appear on real data — a `KeyError` partway through an iteration, backlinks returning a class *name* rather than a class, and the mocked test that passes while `example.gramps` fails. **Translation and locale** covers the two classic reports: strings marked with `_()` that never translate, and an addon that translates on one platform but not another. **Testing** covers the gap between your machine and CI, and between the pre-commit hook and the CI run — they check different things, so green locally is not green upstream. **Pull-request shape** covers what gets a submission bounced rather than reviewed: a rejected version bump, a duplicate, or a PR that simply sits. + +If nothing here matches, the problem is probably not a known pattern — go to [08-debug](08-debug.md) for the technique-level coverage (log levels, repro scripts, pdb, gdb, profilers) and work it from the evidence. For the normative rules an addon must satisfy, see [16-guidelines](16-guidelines.md). ## Loading and discovery @@ -175,6 +179,32 @@ Three common causes: 3. **Test loaded by dotted path surfaces the namespace trap.** Local `discover` from `tests/` would hide the `from import ` bug; CI loads by dotted path (`.tests.`), which exposes it. See bug 12691. +### "`CustomFilters` is `None` in my test." + +`gramps.gen.filters` initialises `CustomFilters = None` at import and only populates it when `reload_custom_filters()` runs — and that function **rebinds the module global** rather than mutating an object in place (`gramps/gen/filters/__init__.py:24`, `:38-41`): + +```python +def reload_custom_filters(): + global CustomFilters + CustomFilters = FilterList(CUSTOM_FILTERS) + CustomFilters.load() +``` + +So importing the *name* first captures the `None` and never sees the replacement: + +```python +# Wrong — binds None for good +from gramps.gen.filters import CustomFilters, reload_custom_filters +reload_custom_filters() + +# Right — reload, then import the name +from gramps.gen.filters import reload_custom_filters +reload_custom_filters() +from gramps.gen.filters import CustomFilters +``` + +Inside Gramps the view manager calls it during startup, which is why this only bites under test. Core's own suites do the same thing at module import time (`gramps/test/test_util.py:41`, `gramps/gen/filters/rules/test/person_rules_test.py:34`). Called out in upstream [`addons-source/AGENTS.md`](https://github.com/gramps-project/addons-source/blob/maintenance/gramps61/AGENTS.md) → Testing. + ### "PR's pre-commit passed but CI is red." Pre-commit catches static checks only. Test failures (e.g. an import that breaks at module load) surface in CI's actual unit-test run, not in pre-commit. After pushing, watch the PR's checks until they finish: diff --git a/docs/addon-development/10-code-analysis.md b/docs/addon-development/10-code-analysis.md index 1ebb28ea4..35865e775 100644 --- a/docs/addon-development/10-code-analysis.md +++ b/docs/addon-development/10-code-analysis.md @@ -13,10 +13,14 @@ is. Cross-reference 15-rules for the normative form. --> -## Overview +## Summary What automated checks run against addon code, locally and in CI, and how to keep an addon passing them. The goal is "PR opens green" — every check below catches a class of issue cheaper than a maintainer review round. +Each check gets a section of its own, in roughly the order it will bite you: **pre-commit** as the harness that runs the rest, then **Black** (formatting), **`mypy`** (static types), **`ruff`** restricted to the E9 / F63 / F7 / F82 rule sets that catch genuine breakage rather than style, **`py_compile` / `ast.parse`** as the syntax floor every file must clear, **`msgfmt`** validating the translation catalogues in `po/`, and **`pylint`** — useful, but scored rather than gated. A separate section covers **verifying `requires_mod`**, which no linter checks: an importable-name mistake there passes every static check and fails only at load time on a user's machine. + +The page then turns to the parts of the coding standard you can check yourself before review, because a human will otherwise raise them: **import grouping**, **callback naming**, **class headers**, **member-name conventions**, and the no-TAB rule. It closes with a **run-everything-locally** recipe — the sequence to execute before pushing so the PR opens green rather than going red and needing a second round. + The checks vary by repo. Two combinations matter; the cheat sheet: | Check | gramps core | addons-source | diff --git a/docs/addon-development/11-internationalization.md b/docs/addon-development/11-internationalization.md index 5405ae82e..7ba2530ea 100644 --- a/docs/addon-development/11-internationalization.md +++ b/docs/addon-development/11-internationalization.md @@ -2,11 +2,13 @@ [← Previous](10-code-analysis.md) · [Index](01-overview.md) · [Next →](12-packaging.md) -## Overview +## Summary Gramps is a highly globalized application, and addons should be fully translatable to support users worldwide. This guide covers how to prepare your addon for internationalization (i18n), manage translation strings using `gettext`, and package translations with your addon. -See [the addon development overview](01-overview.md) for where this fits into the broader addon lifecycle. +Marking strings is the part authors remember; the plumbing around it is where the surprises are, so the page covers both. It opens with a **working example** — the `.gpr.py` and implementation pair side by side — because the translation function is bound differently in each: in `.gpr.py` the loader injects `_` and importing it breaks things, while the implementation module must bind it explicitly through `glocale.get_addon_translator(__file__)`. Getting that one line wrong is the single most common reason an addon's strings never translate. **Translating UI files** covers the Glade case, where the strings live in XML rather than in Python. + +**String marking rules** is the reference part: `_()` for the ordinary case, `N_()` for strings extracted now but translated later at display time, `ngettext` for plurals, and the `"context|String"` convention that lets a translator tell a book's *Title* from a person's *Title* — two words that are identical in English and different in many other languages. The workflow half then splits in two. **Weblate** is the path for Gramps 6.0 and later — translators work in the web platform and you mostly stay out of their way. **Managing translations manually with `make.py`** is the fallback and the mechanism underneath: extracting a `template.pot` from your sources, adding a new language, updating existing catalogues after your strings change, compiling `.po` to `.mo`, and building for release. **Implementation notes** closes with the traps that survive everything else — chief among them that an f-string inside `_()` cannot be extracted at all, so `_("User %s") % name` is right where `_(f"User {name}")` silently yields nothing to translate. ## Working example diff --git a/docs/addon-development/12-packaging.md b/docs/addon-development/12-packaging.md index 4dac98850..2d4a8cede 100644 --- a/docs/addon-development/12-packaging.md +++ b/docs/addon-development/12-packaging.md @@ -17,12 +17,18 @@ branch and gramps61 for master. --> -## Overview +## Summary From "works on my machine" to "users can install it from the addon manager." This chapter is the source-to-distribution pipeline: how `addons-source` becomes a `.addon.tgz` in `addons`, how the in-app addon manager picks it up, and what to send upstream. The normative *rules* a submission must satisfy (branch targeting, version-field discipline, PR body shape, Mantis trailers) live in [16-guidelines](16-guidelines.md). This page covers the *workflow* — what to run, what files appear, where they end up. +The mental model comes first, because the rest only makes sense once you have it: **three repositories** side by side, with `gramps` supplying the API you build against, `addons-source` holding the source you edit, and `addons` holding the built output you never edit by hand. Inside `addons`, each Gramps minor gets its own subdirectory, which is why an addon can support several versions at once. Then the practical setup: the **initial clone** of all three, and the **build prerequisites** — chiefly the `GRAMPSPATH` that points the build at a Gramps checkout. + +`make.py` is the tool that does the work, and the page gives it a **cheat sheet** of targets plus a breakdown of exactly **what `build` packages** into a `.addon.tgz`. The **localisation flow** is a self-contained run through the translation side of packaging: adding a language, refreshing an existing one, the one-time `template.pot` header edit, and — the part people get wrong — which generated files belong in the commit and which are build output that must not be. **Publishing to `addons/`** covers the last mechanical step, where the built package and the refreshed listing JSON land in the repository the in-app addon manager actually reads over HTTPS. + +Two habits get sections of their own. **Edit `addons-source`, not the live plugin directory** is the one that costs people real work: the live plugin directory is a sync *target*, so edits made there are silently overwritten the next time the source syncs across. And the page closes with the two submission paths — **submitting a new addon** and **updating an existing one** — which differ enough in expectations to be worth reading separately. + ## The three repositories ![Fig. 1 — The source-to-distribution pipeline. Authors edit in `addons-source/`; `make.py build` packages each addon into `addons/grampsXY/download/.addon.tgz` and `make.py listing` refreshes `addons/grampsXY/listings/*.json`; the in-app addon manager fetches both over HTTPS and installs to the user's plugin directory. Edits in the user plugin dir are not pushed back — the flow is one-way only.](_media/packaging-pipeline.svg) @@ -85,19 +91,20 @@ See [14-compatibility](14-compatibility.md) for branch-targeting guidance per Gr ## Build prerequisites -`make.py` calls out to two environment things and one OS tool: +`make.py` calls out to one environment variable and one OS tool: -- **`GRAMPSPATH`** — absolute path to your `gramps/` clone. -- **`LANGUAGE`** — must be set to `en_US.UTF-8` for the build to run. +- **`GRAMPSPATH`** — absolute path to your `gramps/` clone, with the *matching* branch checked out (`maintenance/gramps60` in core when building `gramps60` here). `make.py` does have a default (`../../..`), but it assumes one specific workspace layout — always set it explicitly. - **`intltool`** — `sudo apt-get install intltool` on Debian/Ubuntu. The standard invocation: ```bash -GRAMPSPATH=/path/to/gramps LANGUAGE='en_US.UTF-8' python3 make.py gramps60 +GRAMPSPATH=/path/to/gramps python3 make.py gramps60 ``` -Cumbersome to type each time. Set the env vars in your shell startup once; only the `make.py` line varies per command. +**Changed in 6.0**: `LANGUAGE='en_US.UTF-8'` is no longer needed. Older instructions prefix every `make.py` call with it; on 6.0 and later the variable is unused (`make.py` on `maintenance/gramps60` never reads it), and upstream's [`AGENTS.md`](https://github.com/gramps-project/addons-source/blob/maintenance/gramps61/AGENTS.md) says so explicitly. Harmless if it's still in your shell profile. + +Cumbersome to type each time. Set `GRAMPSPATH` in your shell startup once; only the `make.py` line varies per command. In the examples below, `gramps60` is the maintenance/gramps60 target; substitute `gramps61` when you're working on the master branch. @@ -115,8 +122,10 @@ In the examples below, `gramps60` is the maintenance/gramps60 target; substitute | `make.py gramps60 listing ` | Refresh `addons/gramps60/listings/*.json` so the addon manager sees it | | `make.py gramps60 clean ` | Delete generated files (`locale/`, `*.mo`) — run before `git add` | | `make.py gramps60 build all` | Build every addon | +| `make.py gramps60 as-needed` | Build / list / clean only what is out of date, repo-wide | +| `make.py gramps60 manifest-check` | Validate every addon's `MANIFEST` file | -`build` includes `compile`, so the standard release cycle is `clean` → edit → `build` → `listing` → commit + push to `addons/`. +`build` includes `compile`, so the standard release cycle is `clean` → edit → `build` → `listing` → commit + push to `addons/`. `as-needed` is the repo-wide shortcut for that cycle when you don't want to name addons individually. ### What `build` packages diff --git a/docs/addon-development/13-community.md b/docs/addon-development/13-community.md index 9478c8265..e533f1b66 100644 --- a/docs/addon-development/13-community.md +++ b/docs/addon-development/13-community.md @@ -8,10 +8,16 @@ Everything here happens on the wiki / forum / Mantis, not in a repo. --> -## Overview +## Summary When the PR is merged and the package published ([Packaging](12-packaging.md)), the addon *exists* — but nobody can find it, read about it, or reach you about it. This page covers the four steps that make a merged addon part of the ecosystem: the addon-list entry, the addon's own wiki page, the announcement, and the ongoing support duty. None of them touch code; all of them decide whether the addon gets used. +**Listing** means adding a row to the release's addon list on the wiki, copying the skeleton from an existing entry. It is worth understanding why this is separate work: the Plugin Manager's download listing is what *Gramps* reads, while the wiki list is what *people* browse — an addon needs both, and publishing only produces the first. **Documenting** means giving the addon its own wiki page — the page the list's first column links to — following the conventional skeleton of Usage, Features, Prerequisites, and Issues, carrying the standard third-party banner. **Announcing** on the Gramps forum is the step authors skip most often, and an unannounced addon is effectively invisible to the users who would have wanted it. + +**Support** is the open-ended one. Register on the Mantis tracker and check it: nothing routes reports about your addon to you automatically, so they sit unread unless you look. Expect reports to be vague or wrong about the cause — users don't read code — and answer them generously, since a curt reply from an addon's own author undoes the announcement faster than anything else. + +A closing section, **why addons exist**, is worth reading before the maintenance years rather than during them: the addon channel is deliberately low-barrier — the project has never refused an addon — precisely so that experimental, niche, and never-going-into-core work has somewhere to live and evolve. + ## List your addon Add a row for your addon to the release's addon list — [6.0 Addons](https://gramps-project.org/wiki/index.php/6.0_Addons) for the current release, or the next release's list (e.g. [6.1 Addons](https://gramps-project.org/wiki/index.php/6.1_Addons)) if the addon targets an unreleased minor. Copy an existing row and fill in the columns; the [Addon list legend](https://gramps-project.org/wiki/index.php/Addon_list_legend) explains what each column means (type, audience, rating, contact, download). diff --git a/docs/addon-development/14-compatibility.md b/docs/addon-development/14-compatibility.md index 1d5a413fe..b381ecb0b 100644 --- a/docs/addon-development/14-compatibility.md +++ b/docs/addon-development/14-compatibility.md @@ -8,12 +8,18 @@ Cite path:lines or commit/PR# explicitly so this chapter can be audited. --> -## Overview +## Summary How an addon survives — or fails — across Gramps versions. Two things to understand: the `gramps_target_version` contract (Gramps' minor matters; majors aren't even discussed), and the concrete deltas between adjacent maintenance branches that bite ports in practice. A working addon for Gramps 6.0 is usually a working addon for 6.1 with **zero** code changes. The exceptions are documented here; when in doubt, the safest move is to maintain one addon folder per Gramps minor in parallel `maintenance/gramps*` branches of `addons-source`. +The page starts with the contract itself — what **`gramps_target_version`** actually promises, and how to support several minors at once without forking your own code. **Branch targeting for fixes** answers the question that follows immediately: given a bug that affects two releases, which branch does the fix go to, and how does it reach the other. Then the section that earns the page its keep — **"applies cleanly" is not "remains correct"**: a patch that merges without conflict onto another branch can still be wrong there, because the surrounding code has moved. Cherry-picking is not verification. + +**Notable 6.0 → 6.1 deltas** is the concrete inventory for the port most authors face now: plugin discovery following symlinks (which changes the development loop on Linux and macOS), the Windows toolchain move to UCRT64, the GExiv2 version-handling rewrite, and the BSDDB-on-Windows skip. **Reading the deprecation signal** covers how upstream telegraphs that something is going away, so you can act on a warning rather than on a breakage. The page closes with **sanity checks before a port** — the short list to run before claiming an addon supports a new minor. + +This page is the *procedure*; the per-release inventory of what changed is [What's New](15-whats-new.md). + ## `gramps_target_version` semantics The `.gpr.py` registration declares which Gramps minor the addon targets: diff --git a/docs/addon-development/15-whats-new.md b/docs/addon-development/15-whats-new.md index fec2379da..bcb18321b 100644 --- a/docs/addon-development/15-whats-new.md +++ b/docs/addon-development/15-whats-new.md @@ -2,7 +2,7 @@ [← Previous](14-compatibility.md) · [Index](01-overview.md) · [Next →](16-guidelines.md) -## Overview +## Summary API and convention changes that affect addon authors, per Gramps minor release. The audience is someone with a working addon on the previous version asking *"what do I need to know before I bump `gramps_target_version`?"* @@ -14,7 +14,11 @@ This page is the **addon-author slice** of the change log. It's not the full rel - per-addon translation / locale plumbing, - the addon discovery and loading mechanism. -For the practical *how to port* guidance — what to check on a cross-version port, when to maintain parallel branches — see [14-compatibility](14-compatibility.md). This page is the inventory; 14-compatibility is the procedure. +Each release gets its own section, and within it the same four headings: **Added**, **Changed**, **Deprecated**, **Removed**. That shape is deliberate — it maps to the four things a port has to react to. *Added* is opportunity you can ignore safely. *Changed* is where an addon that still runs may now behave differently. *Deprecated* is a deadline you have until the next major release to meet. *Removed* is the one that breaks on load. Reading a release section top to bottom therefore goes from optional to urgent. + +Currently covered: **Gramps 6.1** and **Gramps 6.0** in full, with **earlier releases** summarised rather than enumerated, since an addon that far behind needs a port rather than a change list. **How to read this page** sets out the two conventions worth knowing before you rely on an entry: sections are *incremental*, describing the delta from the previous minor rather than the cumulative surface, and entries carry an inline citation to the upstream commit or PR where one exists, so any claim here can be checked against the source. Entries without a citation record conventions that emerged rather than discrete changes. + +For the current surface — what exists right now, rather than what changed — use [06-api-reference](06-api-reference.md). For the practical *how to port* guidance — what to check on a cross-version port, when to maintain parallel branches — see [14-compatibility](14-compatibility.md). This page is the inventory; 14-compatibility is the procedure. ## Gramps 6.1 diff --git a/docs/addon-development/16-guidelines.md b/docs/addon-development/16-guidelines.md index fa427ad14..23efd8cf9 100644 --- a/docs/addon-development/16-guidelines.md +++ b/docs/addon-development/16-guidelines.md @@ -2,16 +2,24 @@ [← Previous](15-whats-new.md) · [Index](01-overview.md) · [Next →](17-roadmap.md) -## Overview +## Summary Normative reference for addon authors. Conceptual / how-to material lives in the other section pages; this page enumerates the guidelines and is the one to cite in code review. +Rules are stated with RFC 2119 keywords — **MUST** marks a requirement whose violation is a defect, **SHOULD** a strong recommendation you may deviate from with a stated reason, **MAY** an allowance — and where a rule has a known origin, an upstream PR or a maintainer ruling or a Mantis bug, it is cited inline so the rule is auditable rather than folklore. **Repository scope** sets the boundary before any of that: this page governs `addons-source`, not Gramps core, and the two diverge on branch target, test layout, translation tooling, and which static checks are enforced. Where this page is silent, core's rules fill the gap; where it is prescriptive on an addon-specific concern, it wins. Where either disagrees with the authoritative upstream source on the target branch, that source wins. + +The rules themselves run through the life of an addon. **Structure** and **Source location** cover the folder, its name, and where the code belongs. **Translation** covers what must be marked and how. **Runtime** is the densest section and the one worth reading in full even if you skim the rest: every database write inside a `DbTxn`, `requires_mod` naming importable modules rather than PyPI distributions, `requires_gi` pins verified against the branch you target, and a firm list of process-global state an addon must not touch — the GTK main loop, screen-wide CSS, `sys.excepthook`, the root logger, `locale.setlocale`, `os.environ`. An addon is a guest in Gramps' process. **Testing** and **Coding style** cover the test layout and the standard inherited from core. + +The last three sections are about getting a change accepted rather than written: the **contributor workflow**, the **verification to run before commit**, and **commit messages** — including the Mantis trailer keywords that close a tracker issue automatically, and the separate convention for referencing a bug in an addons-source PR body. + ## Repository scope - **This page applies to the addon repository — [`gramps-project/addons-source`](https://github.com/gramps-project/addons-source).** It does **not** govern Gramps core. - Core contributions (`gramps-project/gramps`) follow the separate [Core Development — Rules](https://gramps-project.org/wiki/index.php/Gramps_6.1_Wiki_Manual_-_Core_Development_-_Rules) page. The two repositories diverge on branch target, test layout, translation tooling, and which static checks are enforced — do not transfer a rule across without checking it here. - The full Python coding standard is inherited from core's `../gramps/AGENTS.md`; this page restates the parts addon code review enforces and adds the addon-specific structure, packaging, and translation rules that live outside that file. -- **When in doubt, the authoritative source wins and is what to check.** These pages are a convenience restatement. On coding style, core's `../gramps/AGENTS.md` is the source of truth; on addon-specific rules, the authority is upstream `addons-source` (its `CONTRIBUTING.md` and a maintainer's ruling on the PR). Where this page is silent, ambiguous, or disagrees with the authoritative source on the *target branch*, that source wins — verify against it rather than relying on this page from memory. +- **This page now ships in the repository it governs.** The manual was imported into `addons-source` as `docs/addon-development/` (addons-source PR [994](https://github.com/gramps-project/addons-source/pull/994), merged 2026-07-24, on both `maintenance/gramps60` and `maintenance/gramps61`), and `README.md` / `CONTRIBUTING.md` point at it (PR [995](https://github.com/gramps-project/addons-source/pull/995)). It is no longer only a wiki restatement of upstream practice — upstream carries it. +- **`addons-source/AGENTS.md` and this page split the work deliberately.** The repository's `AGENTS.md` (PR [991](https://github.com/gramps-project/addons-source/pull/991), merged 2026-07-26 on `maintenance/gramps61`) cites `docs/addon-development/16-guidelines.md` for "the normative addon rules — MUST / SHOULD / MAY, with the origin of each rule cited inline" and confines itself to the hands-on workflow around them: `make.py` invocations, how to run the tests, the MSYS2-versus-native-Windows caution, branch selection and fork-PR mechanics. Read `AGENTS.md` for *how to operate the repository*, this page for *what the rules are*. Because the citation runs that way, a contradiction between the two is a defect in upstream's own documentation — report it rather than picking a side. +- **When in doubt, the authoritative source wins and is what to check.** These pages are a convenience restatement. On coding style, core's `../gramps/AGENTS.md` is the source of truth; on addon-specific rules, the authority is upstream `addons-source` — its `CONTRIBUTING.md`, its `AGENTS.md`, and a maintainer's ruling on the PR. Where this page is silent, ambiguous, or disagrees with the authoritative source on the *target branch*, that source wins — verify against it rather than relying on this page from memory. - **Core stands in where this page doesn't — one way only.** Where this page is not specific or prescriptive on a point, the [Core Development — Rules](https://gramps-project.org/wiki/index.php/Gramps_6.1_Wiki_Manual_-_Core_Development_-_Rules) page (and core's `AGENTS.md`) is the default that fills the gap — addons inherit from core. The fallback runs in this direction only: where this page *is* prescriptive on an addon-specific concern (structure, packaging, branch target, test layout — `tests/` + `test_*.py`, `maintenance/gramps60`), it governs and core does not override it; and the addon guidelines never fill a gap in the core page. ## Conventions @@ -33,7 +41,12 @@ Where a rule has a known origin — an upstream PR, a maintainer ruling, a Manti - **MUST**: `fname` points to an implementation module shipped in the same folder. - **MUST**: the addon is physically present under the plugin path — a physical copy works on every Gramps version and OS. (Gramps 6.1+ also discovers an addon reached via a symlink, but a physical copy is the portable default.) - **MUST NOT**: import `register`, `GRAMPLET`, `STABLE`, `_`, or any other name Gramps injects into the `.gpr.py` namespace. -- **MUST NOT**: add `__init__.py` to the addon directory itself. The plugin loader puts the addon dir on `sys.path` and imports `.py` by name; making the addon dir a regular package disturbs that resolution and can trigger the [Mantis 12691](https://gramps-project.org/bugs/view.php?id=12691) submodule-binding trap. (See [07-testing → Why `tests/__init__.py` exists](07-testing.md#why-tests__init__py-exists).) +- **SHOULD NOT**: add `__init__.py` to the addon directory itself — keep the addon root a plain directory, with the `__init__.py` marker only in `tests/`. (See [07-testing → Why `tests/__init__.py` exists](07-testing.md#why-tests__init__py-exists).) + - It does **not** break plugin loading. `PluginManager.import_plugin` inserts the addon's *own* directory at `sys.path[0]` and calls `__import__(mod_name)` — a top-level import — so an addon-root `__init__.py` is never executed on the runtime path (`gramps/gen/plug/_manager.py:300-343`). + - What it costs is consistency across contexts. Run from the addons-source root, as tests are, the same file is also reachable as `.`: two module objects, two copies of every class and module-level global, so identity checks and cached state diverge between the runtime and the test view. The package form also invites `from .module import X`, which fails under the runtime path where that module is top-level — which is exactly why `PostgreSQLEnhanced/__init__.py` needs its `try: from .postgresqlenhanced import … / except ImportError:` `sys.path` fallback. + - **MAY**, therefore, when an addon genuinely needs to expose a package API (a `DATABASE` backend, say) — provided the module named by `fname` imports by bare name and never relatively. + - **Not** the [Mantis 12691](https://gramps-project.org/bugs/view.php?id=12691) trap. That one is `from import ` binding the submodule instead of the class, and it comes from the addon dir's *parent* being on `sys.path`; a namespace package binds the submodule identically. `__init__.py` neither causes it nor cures it — the earlier version of this rule cited 12691 in support, which it does not. + - Seven addons on `maintenance/gramps60` ship one today — `DynamicWeb`, `ExcludeSubtreeFilter`, `GrampyScript`, `PlaceCleanup`, `PostgreSQLEnhanced`, `Query`, `Sqlite` (six of the seven files are empty) — and they work. That is why this is a **SHOULD NOT** and not a **MUST NOT**: a rule seven shipping addons violate without producing a defect does not meet this page's own bar for MUST. - **MUST** (`TOOL` kind): register an `optionclass` even when the tool takes no options. Gramps refuses to load a `TOOL` without one; an empty `tool.ToolOptions` subclass is sufficient. - **SHOULD**: ship a `po/` directory with at least `template.pot` if any user-visible string exists. Generate it with `make.py init ` (see [12-packaging](12-packaging.md)); if it's missing the maintainer creates it on initial check-in. - **MAY**: ship a `tests/` package with an `__init__.py` marker and at least one test — most existing addons predate addon unit tests. When tests are shipped, the `__init__.py` marker keeps dotted-path loading deterministic and the layout rules under *Testing* apply; a bug fix still **SHOULD** ship a regression test. @@ -49,7 +62,16 @@ The full how-to (registration setup, `make.py` lifecycle, Glade runtime-override - **MUST**: wrap every user-visible string with `_()`. - **MUST NOT**: `import _` in `.gpr.py` — Gramps' plugin loader injects it. Implementation modules **MUST** bind it explicitly via `_ = glocale.get_addon_translator(__file__).gettext`. -- **MUST** (multi-file packages): when the addon's code is split across a nested package, bind `_` **once at the addon root** — the directory that holds `locale/`, in a root-level module (e.g. `_i18n.py`) — and import it everywhere else by **bare name** (`from _i18n import _`), **not** a `.`-prefixed path: the addon dir is on `sys.path` and its root is **not** a package (see *Structure* → MUST NOT `__init__.py`), so a root-level module imports directly, whereas `from ..i18n import _` raises `'' is not a package` at import time. `get_addon_translator(filename)` derives the catalog dir as `dirname(abspath(filename)) + "/locale"` (`gramps.gen.utils.grampslocale`), so a `get_addon_translator(__file__)` call from a nested module (e.g. `myaddon/views/tab.py`) resolves `myaddon/views/locale/`, which doesn't exist, and a non-English user silently gets the untranslated string. The flat `_ = glocale.get_addon_translator(__file__).gettext` form above is correct only because that module sits at the addon root; from a nested module, anchor the path at the root (e.g. `get_addon_translator(os.path.join(ADDON_ROOT, "_"))` — only `dirname(...)` is read, so the basename is an unused placeholder) instead of passing `__file__`. (NameSuite i18n-anchor fix, 2026-06-25.) +- **SHOULD**: guard that binding so an addon with no compiled catalog still imports: + ```python + try: + _trans = glocale.get_addon_translator(__file__) + except ValueError: + _trans = glocale.translation + _ = _trans.gettext + ``` + `get_addon_translator` reaches `GrampsLocale._get_translation`, which **raises `ValueError("No usable translations in …")`** when no `.mo` is found for any language in the list and none of them starts with `en` or `C` (`gramps/gen/utils/grampslocale.py:536-540`, unchanged on `maintenance/gramps60` and `master`). An English-locale user never sees it, so the unguarded form passes every test on a developer machine and raises at import time for the first user running a non-English UI against an addon whose `locale/` isn't built yet. Prescribed by upstream [`addons-source/AGENTS.md`](https://github.com/gramps-project/addons-source/blob/maintenance/gramps61/AGENTS.md) → Internationalization. +- **MUST** (multi-file packages): when the addon's code is split across a nested package, bind `_` **once at the addon root** — the directory that holds `locale/`, in a root-level module (e.g. `_i18n.py`) — and import it everywhere else by **bare name** (`from _i18n import _`), **not** a `.`-prefixed path: at load time the addon dir *itself* is what Gramps puts on `sys.path`, so `` is not an importable name from inside the addon at all (see *Structure* → `__init__.py`), and a root-level module imports directly whereas `from ..i18n import _` raises `'' is not a package` at import time. This holds whether or not the addon root carries an `__init__.py` — the runtime path never has the addon's *parent* on `sys.path`. `get_addon_translator(filename)` derives the catalog dir as `dirname(abspath(filename)) + "/locale"` (`gramps.gen.utils.grampslocale`), so a `get_addon_translator(__file__)` call from a nested module (e.g. `myaddon/views/tab.py`) resolves `myaddon/views/locale/`, which doesn't exist, and a non-English user silently gets the untranslated string. The flat `_ = glocale.get_addon_translator(__file__).gettext` form above is correct only because that module sits at the addon root; from a nested module, anchor the path at the root (e.g. `get_addon_translator(os.path.join(ADDON_ROOT, "_"))` — only `dirname(...)` is read, so the basename is an unused placeholder) instead of passing `__file__`. (NameSuite i18n-anchor fix, 2026-06-25.) - **SHOULD**: verify an addon translation against an **addon-owned** msgid — one that appears only in the addon's `template.pot`, never a string that also exists in core (e.g. `"Given name"`). `get_addon_translator` returns the **core** translator with the addon catalog only as a *fallback*, so a core string renders translated whether or not the addon binding resolves — it cannot prove the fix. (Same fix: the original check used a core string and demonstrated nothing.) - **MUST NOT**: wrap an f-string or `.format()` result in a translation function. `xgettext` cannot extract dynamically built strings. - **Bad:** `_(f"User {name}")`, `_("User {}".format(name))` @@ -175,7 +197,7 @@ addons-source PRs don't use `Fixes #NNNN` in the commit message — that trailer - [Overview](01-overview.md) - [Fundamentals](04-fundamentals.md) -- [Testing](https://gramps-project.org/wiki/index.php/Gramps_6.0_Wiki_Manual_-_Addon_Testing) +- [Testing](07-testing.md) - [Code analysis](10-code-analysis.md) - [Packaging](12-packaging.md) - `../gramps/AGENTS.md` — the full Python coding standard inherited here. diff --git a/docs/addon-development/17-roadmap.md b/docs/addon-development/17-roadmap.md index bec8e65ff..c08545953 100644 --- a/docs/addon-development/17-roadmap.md +++ b/docs/addon-development/17-roadmap.md @@ -2,11 +2,17 @@ [← Previous](16-guidelines.md) · [Index](01-overview.md) -## Overview +## Summary Forward-looking view of the addon-development surface — what's planned, what's in flight, what's slated for deprecation, and what open questions will eventually become rules. The audience is an addon author asking "what do I need to plan around?" -This page is the **prospective** counterpart to [What's new](https://gramps-project.org/wiki/index.php/Gramps_6.0_Wiki_Manual_-_Addon_Development_-_Whats_New), which is retrospective. An item moves from this page to *What's new* once it ships in a release. +This page is the **prospective** counterpart to [What's new](15-whats-new.md), which is retrospective. An item moves from this page to *What's new* once it ships in a release. + +**How to read this page** comes first because entries here carry less certainty than elsewhere in the section, and the four fields on each — **Status**, **Target**, **Impact**, **Tracking** — are what let you judge how much weight to put on one. The rule the page holds itself to is worth knowing as a reader: an entry without a tracking link is a wish rather than a plan, and belongs somewhere other than a roadmap. + +Entries are then grouped by how settled they are. **In flight** is work already underway upstream. **Accepted but not yet implemented** is agreed in principle with nobody currently on it — the section where volunteering is most useful. **Deprecations and removals** is the one to check against your own addon, since it is where your future breakage is announced. **Open questions** are the genuinely undecided ones, which is also the invitation to weigh in before they harden into rules. **Deferred / rejected** exists so that a proposal that keeps resurfacing has a recorded answer and the reasoning behind it. + +A final section tracks the **documentation roadmap** — the state of this manual itself, page by page, and the publishing-pipeline conventions behind it. That is meta rather than API, but it is where to look before proposing a documentation change. ## How to read this page @@ -19,7 +25,7 @@ Each entry should answer four things: | **Impact** | what addon authors need to do (rewrite / opt-in / nothing) | | **Tracking** | PR / Mantis bug / wiki RFC / mailing-list thread | -A roadmap entry without a tracking link is a wish, not a plan; either add the link or move the entry to a separate "ideas" section. +A roadmap entry without a tracking link is a wish, not a plan; either add the link or move the entry to a separate "ideas" section. *Open questions* are the one exception: a question that has not been raised upstream yet has nothing to link, and says so in its **Tracking** field rather than borrowing a citation that doesn't cover it. ## In flight @@ -55,7 +61,11 @@ A roadmap entry without a tracking link is a wish, not a plan; either add the li - "Should addon `version` be author-managed or maintainer-managed per repo?" --> -- _none recorded yet_ +- **Should CI flag an addon-root `__init__.py`?** Follows from the rule being a SHOULD NOT rather than a MUST NOT ([16-guidelines → Structure](16-guidelines.md#structure)). + - **Status** open · **Target** unscheduled · **Impact** none for authors unless it becomes gating · **Tracking** none yet — raise on `addons-source` before acting + - The case for a check: the seven current instances are mostly accidental — six of the seven files are empty, so nothing depends on them, and an advisory flag would stop the pattern spreading by imitation. + - The case against gating on it: `maintenance/gramps60` has seven violations today, so a blocking check starts red and pressures a change to working addons for no functional gain. A lint also cannot tell the deliberate case (`PostgreSQLEnhanced`, which exposes a package API and needs its `__init__.py`) from a cargo-culted empty file. + - If it lands, the shape that fits the rule's actual strength is **advisory** — report the file list, don't fail the build — or a gate with the existing seven grandfathered by allowlist. Gating a SHOULD is a contradiction; either the rule rises back to MUST on new evidence, or the check stays a warning. ## Deferred / rejected @@ -68,7 +78,7 @@ A roadmap entry without a tracking link is a wish, not a plan; either add the li ## Documentation roadmap -The doc set itself is in flight. Pages with `managed: false` front-matter are draft stubs and will not appear in published output until promoted. Current draft state — flip to `managed: true` page by page as content lands: +The state of this manual itself. All seventeen pages are `managed: true` and publish; the only `managed: false` file in the section is the vault-internal sidebar. A page added later starts `managed: false` and is promoted once its content lands. ### Publishing-pipeline conventions (now supported) @@ -90,6 +100,16 @@ What the pipeline already handled before these additions: - YAML front-matter → `title`, `categories`, `managed`. - Fenced code with language tags, tables. +### Second publish target: `addons-source` + +Since 2026-07-24 this manual has two homes. Alongside the wiki, it is exported to GitHub-native Markdown and lives in the repository it documents, as `docs/addon-development/` on both `maintenance/gramps60` and `maintenance/gramps61` (addons-source PR [994](https://github.com/gramps-project/addons-source/pull/994); `README.md` and `CONTRIBUTING.md` point at it per PR [995](https://github.com/gramps-project/addons-source/pull/995)). The repository's `AGENTS.md` cites `docs/addon-development/16-guidelines.md` as the normative rule set (PR [991](https://github.com/gramps-project/addons-source/pull/991)). + +Consequences for anyone editing these pages: + +- The **vault stays the source of truth**; the in-repo copy is generated, never hand-edited. Drift is detected by re-running the exporter and diffing in the target checkout before committing. +- A rules change here now lands in a repository whose agent guidelines *cite* it, so an error propagates further than the wiki. Verify a rule against the repository's actual implementation before stating it — the pins-location correction (per-addon versus repo-root `tests/__init__.py`) came from exactly this failure. +- Where `AGENTS.md` and [Rules](16-guidelines.md) disagree, upstream's own documentation is inconsistent; the fix is a report upstream, not a silent divergence here. + ### Page-by-page state The section is substantive across all seventeen pages. Open deepening work: @@ -100,7 +120,7 @@ The section is substantive across all seventeen pages. Open deepening work: ## See also -- [What's new](https://gramps-project.org/wiki/index.php/Gramps_6.0_Wiki_Manual_-_Addon_Development_-_Whats_New) — retrospective counterpart. +- [What's new](15-whats-new.md) — retrospective counterpart. - [Compatibility](14-compatibility.md) — porting guidance once an item ships. - [Mantis bug tracker](https://gramps-project.org/bugs) — feature requests and design discussions originate here. - [Gramps mailing lists](https://gramps-project.org/contact/) — where larger design questions get hashed out.