|
| 1 | +# Agent Guidelines for gramps-project/addons-source |
| 2 | + |
| 3 | +This document specifies rules and conventions that agents should follow when |
| 4 | +making changes to this repository. |
| 5 | + |
| 6 | +For the normative addon rules — MUST / SHOULD / MAY, with the origin of |
| 7 | +each rule (upstream PR, maintainer ruling, Mantis id) cited inline — see |
| 8 | +[docs/addon-development/16-guidelines.md](docs/addon-development/16-guidelines.md) |
| 9 | +from the Addon Development manual (PR 994). This document covers the |
| 10 | +hands-on agent workflow around those rules. |
| 11 | + |
| 12 | +## Repository Overview |
| 13 | + |
| 14 | +`addons-source` holds the source code for third-party Gramps addons — |
| 15 | +gramplets, views, tools, reports, importers/exporters, filter rules, database |
| 16 | +backends, and other plugins that are not part of Gramps core. It is one of |
| 17 | +three tightly-linked repositories: |
| 18 | + |
| 19 | +- **`gramps`** — Gramps core (the application these addons plug into). |
| 20 | +- **`addons-source`** (this repo) — addon source code, developed here. |
| 21 | +- **`addons`** — built `.addon.tgz` packages and listing JSON consumed by the |
| 22 | + in-app Plugin Manager. Populated from this repo via `make.py`; not edited |
| 23 | + by hand except by the maintainer doing a release build. |
| 24 | + |
| 25 | +There is no unified build or CI pipeline for the whole repo (the checked-in |
| 26 | +`.travis.yml` is stale/unused) — each addon is largely independent. |
| 27 | + |
| 28 | +## Branch Model |
| 29 | + |
| 30 | +Branches are per Gramps major or feature release, e.g. `maintenance/gramps60`, |
| 31 | +`maintenance/gramps61` — not one branch per patch release. **Almost all addon |
| 32 | +work should target the maintenance branch matching the current released |
| 33 | +Gramps version.** A `master` branch exists but is not currently used for |
| 34 | +addon work. |
| 35 | + |
| 36 | +Pick the branch carefully: |
| 37 | +- When creating a new addon or fixing an existing one, branch from |
| 38 | + `origin/maintenance/gramps6X` (the current release line), not from a local |
| 39 | + tracking branch that may carry unrelated WIP commits. |
| 40 | +- When opening the PR, target that same branch on |
| 41 | + `gramps-project/addons-source`. |
| 42 | +- Before pushing, sync/rebase against upstream (`git pull --rebase`) so the |
| 43 | + PR applies cleanly. |
| 44 | + |
| 45 | +All PRs must come from a personal fork, never from a branch pushed directly |
| 46 | +to `gramps-project/addons-source` (see `CONTRIBUTING.md`, "Commit Your |
| 47 | +Changes": "you want to put your changes into _your_ fork, _not_ the upstream |
| 48 | +`gramps-project/addons-source` repository"). |
| 49 | + |
| 50 | +Weblate translation PRs are the one exception where commits must **not** be |
| 51 | +squashed on merge; everything else can be merged/rebased normally. |
| 52 | + |
| 53 | +## Repository Layout |
| 54 | + |
| 55 | +Each addon is a single top-level directory, CamelCase-named to match its |
| 56 | +Python import name (e.g. `FilterRules/`, `DataEntryGramplet/`). A directory |
| 57 | +typically contains: |
| 58 | + |
| 59 | +- `AddonName.py` — the implementation. |
| 60 | +- `AddonName.gpr.py` — the Gramps plugin registration file (required; see |
| 61 | + below). |
| 62 | +- `po/` — translation `.po` files, managed by `make.py`. |
| 63 | +- `tests/` — optional unit tests (see Testing below). |
| 64 | +- `MANIFEST` — optional; lists extra files (docs, data) to include in the |
| 65 | + built package beyond the default `*.py`, `*.glade`, `*.xml`, `*.txt`, |
| 66 | + `locale/*/LC_MESSAGES/*.mo`. |
| 67 | +- `locale/` — generated `.mo` files; not something you hand-edit. |
| 68 | + |
| 69 | +Addon directories generally have **no `__init__.py`** (they rely on Python 3 |
| 70 | +namespace packages) so that Gramps can add each one to `sys.path` at load |
| 71 | +time and other addons can `import AddonName` directly. |
| 72 | + |
| 73 | +## Commands |
| 74 | + |
| 75 | +Addon packaging/translation tasks go through `make.py`, run from the repo |
| 76 | +root. Point `GRAMPSPATH` at wherever *your* local Gramps checkout lives (it |
| 77 | +defaults to `../../..`, i.e. it assumes `make.py` is being invoked from one |
| 78 | +level inside a sibling-layout workspace — don't rely on that default, always |
| 79 | +set it explicitly), and make sure that checkout has the matching branch |
| 80 | +checked out too (e.g. `maintenance/gramps61` in core when building against |
| 81 | +`gramps61` here). Setting `LANGUAGE=en_US.UTF-8` is no longer required on |
| 82 | +Gramps v6.0 and later: |
| 83 | + |
| 84 | +```bash |
| 85 | +GRAMPSPATH=/path/to/your/gramps python3 make.py gramps61 build AddonDirectory |
| 86 | +``` |
| 87 | + |
| 88 | +Common subcommands (first positional arg is the branch/version tag, e.g. |
| 89 | +`gramps61`): |
| 90 | + |
| 91 | +- `init AddonDirectory [lang]` — scaffold dirs / `.pot` for a new addon, or |
| 92 | + an initial `po/<lang>-local.po`. |
| 93 | +- `update AddonDirectory <lang>` — refresh a language `.po` from the `.pot`. |
| 94 | +- `compile [AddonDirectory|all]` — compile `.po` → `.mo`. |
| 95 | +- `build [AddonDirectory|all]` — produce the downloadable `.addon.tgz`. |
| 96 | +- `listing [AddonDirectory|all]` — generate/update the Plugin Manager |
| 97 | + listing JSON. |
| 98 | +- `clean AddonDirectory` — strip generated files (`locale/`, `*.pot`, etc.) |
| 99 | + before committing. |
| 100 | +- `manifest-check` — validate `MANIFEST` files. |
| 101 | +- `as-needed` — build/list/clean only what's out of date, repo-wide. |
| 102 | + |
| 103 | +Unlike `GRAMPSPATH`, the `build`/`listing`/`check` subcommands write to a |
| 104 | +**hardcoded relative path**, `../addons/<version>/...` — this isn't a |
| 105 | +convention, it's how `make.py` itself resolves the output location, so it |
| 106 | +only works if you've cloned the `addons` repo as a sibling of |
| 107 | +`addons-source` (see `MAINTAINERS.md` for the recommended three-repo |
| 108 | +workspace layout: `gramps` / `addons` / `addons-source` side by side). You |
| 109 | +don't need that sibling checkout at all unless you're packaging/publishing |
| 110 | +an addon. |
| 111 | + |
| 112 | +You do not need `make.py` just to write/test an addon's Python code — only |
| 113 | +when packaging, translating, or updating the listing. |
| 114 | + |
| 115 | +## Testing |
| 116 | + |
| 117 | +Not every addon has tests, but when adding or fixing one, add regression |
| 118 | +tests under `AddonName/tests/`, mirroring existing examples (`Form/tests/`, |
| 119 | +`DataEntryGramplet/tests/`, `Sqlite/tests/`, `FilterRules/tests/`). |
| 120 | + |
| 121 | +Conventions here differ from Gramps core: |
| 122 | + |
| 123 | +- Test files are named `test_*.py` (pytest-style), **not** the `*_test.py` |
| 124 | + suffix core Gramps uses — `unittest discover` here is invoked per-addon or |
| 125 | + from the repo root without the core `-p "*_test.py"` pattern. |
| 126 | +- Still use the `unittest` framework (`unittest.TestCase`), not `pytest` |
| 127 | + APIs, even though filenames follow the `test_*.py` convention. |
| 128 | +- `AddonName/tests/__init__.py` should be **empty**. Do not add |
| 129 | + `gi.require_version(...)` pinning there or in individual test modules — |
| 130 | + see GTK/GDK below. |
| 131 | +- Because the addon directory has no `__init__.py`, test modules need a |
| 132 | + `sys.path` hack to import it as a namespace package: |
| 133 | + |
| 134 | + ```python |
| 135 | + ADDON_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) |
| 136 | + if ADDON_DIR not in sys.path: |
| 137 | + sys.path.insert(0, ADDON_DIR) |
| 138 | + ``` |
| 139 | + |
| 140 | + then `from AddonName.addonmodule import SomeClass`. |
| 141 | +- Build test data programmatically against a real (temp-dir) database rather |
| 142 | + than relying on fixtures that don't exist in this repo: |
| 143 | + |
| 144 | + ```python |
| 145 | + from gramps.gen.db.utils import make_database |
| 146 | + from gramps.gen.db import DbTxn |
| 147 | + |
| 148 | + db = make_database("sqlite") |
| 149 | + db.load(tempfile.mkdtemp(prefix="myaddon_")) |
| 150 | + with DbTxn("build test db", db) as txn: |
| 151 | + ... |
| 152 | + ``` |
| 153 | +- If a test needs `gramps.gen.filters.CustomFilters`, call |
| 154 | + `reload_custom_filters()` **before** importing the `CustomFilters` name |
| 155 | + from `gramps.gen.filters` — that function rebinds the module-level global |
| 156 | + rather than mutating it in place, so importing the name first leaves you |
| 157 | + with a stale `None`. |
| 158 | + |
| 159 | +### GTK / GDK version pinning |
| 160 | + |
| 161 | +The repo-root `tests/__init__.py` pins `gi.require_version("Gtk", "3.0")` and |
| 162 | +`gi.require_version("Gdk", "3.0")` for the whole test suite (added in PR |
| 163 | +#950). `gi.require_version` sets process-global state, so **individual addon |
| 164 | +test files should not re-pin GTK/GDK** — that's now redundant. A new test |
| 165 | +module that imports something pulling in `gramps.gui.*` (which happens at |
| 166 | +import time for most GUI-facing addons) only needs a guard for hosts with no |
| 167 | +PyGObject at all: |
| 168 | + |
| 169 | +```python |
| 170 | +try: |
| 171 | + import gi |
| 172 | +except ImportError as err: |
| 173 | + raise unittest.SkipTest("PyGObject not available: %s" % err) |
| 174 | +``` |
| 175 | + |
| 176 | +Do not add `gi.require_version(...)` calls back into new test files — if you |
| 177 | +see them in an addon you're touching, they're safe to remove as redundant |
| 178 | +(see the `Themes` addon's `tests/__init__.py` cleanup for precedent), but |
| 179 | +don't remove them from files you aren't otherwise changing. |
| 180 | + |
| 181 | +### Running tests |
| 182 | + |
| 183 | +```bash |
| 184 | +export GRAMPS_RESOURCES=/path/to/gramps/build/share # built Gramps checkout |
| 185 | +export GDK_BACKEND=- |
| 186 | +python3 -m unittest AddonName.tests.test_something -v |
| 187 | +``` |
| 188 | + |
| 189 | +Run from the `addons-source` repo root so the addon's namespace package |
| 190 | +resolves. |
| 191 | + |
| 192 | +## Windows Development |
| 193 | + |
| 194 | +Per `CONTRIBUTING.md`, addons here are developed and tested primarily on |
| 195 | +Linux; Windows and macOS are possible but far less thoroughly reviewed. |
| 196 | +"Windows" itself covers two different things agents should not conflate: |
| 197 | + |
| 198 | +- **MSYS2** — the POSIX-like shell/toolchain environment Gramps's own |
| 199 | + Windows installer is built with. A shell running inside MSYS2 behaves like |
| 200 | + a Linux shell (`bash`, POSIX paths) and can run this repo's `make.py` and |
| 201 | + `unittest` invocations normally. |
| 202 | +- **Native Windows** — a bare `cmd.exe` or PowerShell host with no MSYS2 or |
| 203 | + WSL layer underneath. The commands and paths used elsewhere in this |
| 204 | + document (`make.py`, `python3 -m unittest ...`, the `GRAMPSPATH` / |
| 205 | + `GRAMPS_RESOURCES` env vars) assume a POSIX shell and are not verified |
| 206 | + against native `cmd`/PowerShell. |
| 207 | + |
| 208 | +If you are running as an agent on a native Windows host: |
| 209 | + |
| 210 | +- Reading, writing, and refactoring addon Python code is fine — the code |
| 211 | + itself is portable. |
| 212 | +- Do not run this repo's tests or `make.py` directly in `cmd`/PowerShell and |
| 213 | + report the result as a real pass/fail — the tooling isn't validated there, |
| 214 | + so a failure may be environmental noise rather than an actual regression. |
| 215 | +- Ask the user to run the same command inside an MSYS2 shell, WSL, or a |
| 216 | + Linux container/VM and share the output, rather than guessing at the |
| 217 | + result yourself. |
| 218 | + |
| 219 | +If you're already inside MSYS2 or WSL, treat it as a POSIX shell and follow |
| 220 | +the rest of this document (Commands, Testing) as written. |
| 221 | + |
| 222 | +## Code Style |
| 223 | + |
| 224 | +- Every new `.py` file needs the same GPL-2.0-or-later header with copyright |
| 225 | + used throughout Gramps core. |
| 226 | +- Group imports under comment-banner sections (`Standard Python modules`, |
| 227 | + `GTK/Gnome modules`, `Gramps modules`), matching the style already used |
| 228 | + across this repo (e.g. `FilterRules/isfamilyfiltermatchevent.py`). |
| 229 | +- [Black](https://black.readthedocs.io/) formatting is not currently required |
| 230 | + for addons — there's no checked-in CI workflow enforcing it here, though |
| 231 | + that may change in the future. The existing codebase is largely |
| 232 | + Black-formatted, so running `black <file>` on files you're already |
| 233 | + touching is a reasonable default. |
| 234 | +- Docstrings: concise; full Sphinx `:param:`/`:returns:` style only when the |
| 235 | + parameters aren't already obvious from the signature. |
| 236 | + |
| 237 | +## Internationalization |
| 238 | + |
| 239 | +Addons manage their own translations, separately from Gramps core, though |
| 240 | +both go through the same Weblate instance: strings already translated in |
| 241 | +Gramps core are matched first, so those strings are excluded from the |
| 242 | +Weblate _Addons_ component and don't need separate translation there. At the |
| 243 | +top of an addon's main module: |
| 244 | + |
| 245 | +```python |
| 246 | +from gramps.gen.const import GRAMPS_LOCALE as glocale |
| 247 | + |
| 248 | +try: |
| 249 | + _trans = glocale.get_addon_translator(__file__) |
| 250 | +except ValueError: |
| 251 | + _trans = glocale.translation |
| 252 | +_ = _trans.gettext |
| 253 | +``` |
| 254 | + |
| 255 | +(`get_addon_translator` raises `ValueError` if the addon has no |
| 256 | +`locale/` translations yet — fall back to the core translator rather than |
| 257 | +letting the import crash.) Use `ngettext(singular, plural, n)` instead of a |
| 258 | +single `_()` call for any string that counts a noun, for the same reason as |
| 259 | +Gramps core: many languages have plural rules English doesn't, and only |
| 260 | +`ngettext` lets gettext apply the target language's actual rule. |
| 261 | + |
| 262 | +## Plugin Registration (`.gpr.py`) |
| 263 | + |
| 264 | +Every addon needs an `AddonName.gpr.py` alongside `AddonName.py`: |
| 265 | + |
| 266 | +```python |
| 267 | +register( |
| 268 | + RULE, # or TOOL, GRAMPLET, REPORT, VIEW, GENERAL, IMPORT, EXPORT, ... |
| 269 | + id="uniqueid", |
| 270 | + name=_("Human-Readable Name"), |
| 271 | + description=_("What it does."), |
| 272 | + version="1.0.0", |
| 273 | + gramps_target_version="6.1", |
| 274 | + status=STABLE, # or UNSTABLE |
| 275 | + fname="AddonName.py", |
| 276 | + authors=["Author Name"], |
| 277 | + authors_email=["author@example.com"], |
| 278 | + maintainers=["Maintainer Name"], |
| 279 | + maintainers_email=["maintainer@example.com"], |
| 280 | + help_url="Addon:WikiPageName", |
| 281 | + ... # PTYPE-specific attributes, e.g. ruleclass/namespace for RULE |
| 282 | +) |
| 283 | +``` |
| 284 | + |
| 285 | +`gramps_target_version` and `version` are required. `help_url` should point |
| 286 | +at the addon's Gramps wiki page (`Addon:PageName`), not a GitHub URL. |
| 287 | +`maintainers`/`maintainers_email` identify who currently maintains the |
| 288 | +addon and may differ from `authors`/`authors_email` (the original author) |
| 289 | +once upkeep passes to someone else. |
| 290 | + |
| 291 | +> **Do not manually change `version` in a PR.** The `MAJOR.MINOR.PATCH` |
| 292 | +> version in `.gpr.py` is bumped automatically by the `addons` repo's |
| 293 | +> packaging build (`make.py`) when a change is released — the patch |
| 294 | +> component increments on its own. Hand-editing it in a source PR just |
| 295 | +> creates spurious diffs and can conflict with what the release build |
| 296 | +> assigns; leave it as-is unless you're intentionally doing a MAJOR/MINOR |
| 297 | +> bump for a breaking addon change. |
| 298 | +
|
| 299 | +## Bug Reports and Commit Messages |
| 300 | + |
| 301 | +Bugs against addons are tracked in two places: the shared |
| 302 | +[Gramps Mantis BT](https://gramps-project.org/bugs/view_all_bug_page.php) |
| 303 | +and, informally, the [Gramps Discourse forum](https://gramps.discourse.group/) |
| 304 | +— many addon issues start as a discourse thread rather than a Mantis report. |
| 305 | +Unlike Gramps core, this repo has no automated changelog tooling parsing |
| 306 | +commit messages, so there's no required `Fixes #N` keyword syntax — just |
| 307 | +describe *why* the change was made, and link whatever report (Mantis bug or |
| 308 | +discourse URL) motivated it if one exists. Don't invent a Mantis bug number |
| 309 | +if you only have a discourse link or user report — link what you actually |
| 310 | +have. |
0 commit comments