Every plugin Postulo ships is its own package, core included #129

Closed
opened 2026-09-09 10:15:51 +00:00 by tiagoagueda · 1 comment
Owner

Observation

and imperative is that a plugin, even a internal must be a self-contained as possible
having is own manifest, is own locale, etc, and dont depende on the core

What exists

Three parts to the imperative, and they are in three different states.

Manifests: done. #97 and #98 settled it. Every built-in declares one through
@declares(shipped(...)), and tests/test_plugin_manifests.py walks all seven and fails on
one that says nothing about itself. manifest_of() looks in the manifest, then the loose
attributes plugins used before there was one, then the wheel the plugin was installed from.

Locale: false for every plugin Postulo ships. docs/PLUGINS.md says a plugin's strings
are never added to Postulo's catalogues. There is one locale directory in the repository
and every built-in's strings are in it, translated into 39 languages as part of Postulo's
own release.

Independence: not attempted, and unevenly possible. Where the built-ins actually live:

Plugin Lives in Imports from postulo
schema.org, page-metadata plugins/builtin.py none
europass resume/importers.py yes, and its templates are in templates/resume/
email, smtp notifications/ yes
local documents/stores.py yes
phone-numbers core/features.py yes — and the feature itself is a core model, a core service module, two core forms, a core template partial and two core views

The two sources are already what the imperative describes. The feature is the furthest from
it, and it is the newest, which says something about which direction the code drifts when
nothing pulls the other way.

What this asks for

Every plugin Postulo ships becomes its own package: its own manifest (has one), its own
locale/, its own templates, its own registration through an entry point, and imports only
from a declared surface rather than from wherever in core the thing happens to be.

Worth being careful about

"As self-contained as possible" is the operative phrase, and where the line falls is the
work.
A plugin holding one person's data must scope it with for_user() or it breaks the
project's first promise. One that renders must extend the base template or it renders
unstyled. Those are reasons to depend on Postulo, not failures of discipline — which is why
the first prerequisite is a named surface rather than a rule against importing.

A rule nobody checks drifts back. phone-numbers was written a week ago, entirely
inside core, by somebody who had just read the plugin documentation. That is the argument
for a test that walks each built-in package and fails on an import past the surface — the
same shape as the manifest test that already exists.

Staging is available and worth taking. The two sources satisfy the imperative today; the
importer and the transport hold no data and could follow with only the surface and the
locale questions answered. Only the store, the notifier and the feature need the data
question settled first, so this need not be one large change.

Moving a template moves a translation. Extracted strings are keyed by their source
location; moving a built-in's templates and modules into a package rewrites references
throughout all 39 catalogues even where no string changes. That is churn the catalogues
already tolerate, but it wants to happen in one commit per plugin, not spread over several.

shipped() says these are Postulo's, taking the version, author, licence and source URL
from the application. That is truthful for a built-in and worth keeping when it moves: a
package that ships inside the image is not a third party and should not claim to be one.

#125 changes because of this. That issue offers a cheap answer for "internal widgets
plugin" — name the existing registry as one and give it an entry point when third parties
arrive. Under this imperative that answer is no longer available: seventeen widgets, each
with its own template and context, would be seventeen packages or one, and either way the
locale and surface questions arrive with them.

This is not a user-visible change, which makes it the kind of work that is easy to
justify and easy to leave half-finished. The measure of it is not that the directories
moved: it is that the next phone-numbers cannot be written inside core without a test
saying so.

## Observation > and imperative is that a plugin, even a internal must be a self-contained as possible > having is own manifest, is own locale, etc, and dont depende on the core ## What exists Three parts to the imperative, and they are in three different states. **Manifests: done.** #97 and #98 settled it. Every built-in declares one through `@declares(shipped(...))`, and `tests/test_plugin_manifests.py` walks all seven and fails on one that says nothing about itself. `manifest_of()` looks in the manifest, then the loose attributes plugins used before there was one, then the wheel the plugin was installed from. **Locale: false for every plugin Postulo ships.** `docs/PLUGINS.md` says a plugin's strings are *never* added to Postulo's catalogues. There is one locale directory in the repository and every built-in's strings are in it, translated into 39 languages as part of Postulo's own release. **Independence: not attempted, and unevenly possible.** Where the built-ins actually live: | Plugin | Lives in | Imports from `postulo` | | --- | --- | --- | | `schema.org`, `page-metadata` | `plugins/builtin.py` | **none** | | `europass` | `resume/importers.py` | yes, and its templates are in `templates/resume/` | | `email`, `smtp` | `notifications/` | yes | | `local` | `documents/stores.py` | yes | | `phone-numbers` | `core/features.py` | yes — and the feature itself is a core model, a core service module, two core forms, a core template partial and two core views | The two sources are already what the imperative describes. The feature is the furthest from it, and it is the newest, which says something about which direction the code drifts when nothing pulls the other way. ## What this asks for Every plugin Postulo ships becomes its own package: its own manifest (has one), its own `locale/`, its own templates, its own registration through an entry point, and imports only from a declared surface rather than from wherever in core the thing happens to be. ## Worth being careful about **"As self-contained as possible" is the operative phrase, and where the line falls is the work.** A plugin holding one person's data must scope it with `for_user()` or it breaks the project's first promise. One that renders must extend the base template or it renders unstyled. Those are reasons to depend on Postulo, not failures of discipline — which is why the first prerequisite is a named surface rather than a rule against importing. **A rule nobody checks drifts back.** `phone-numbers` was written a week ago, entirely inside core, by somebody who had just read the plugin documentation. That is the argument for a test that walks each built-in package and fails on an import past the surface — the same shape as the manifest test that already exists. **Staging is available and worth taking.** The two sources satisfy the imperative today; the importer and the transport hold no data and could follow with only the surface and the locale questions answered. Only the store, the notifier and the feature need the data question settled first, so this need not be one large change. **Moving a template moves a translation.** Extracted strings are keyed by their source location; moving a built-in's templates and modules into a package rewrites references throughout all 39 catalogues even where no string changes. That is churn the catalogues already tolerate, but it wants to happen in one commit per plugin, not spread over several. **`shipped()` says these are Postulo's**, taking the version, author, licence and source URL from the application. That is truthful for a built-in and worth keeping when it moves: a package that ships inside the image is not a third party and should not claim to be one. **#125 changes because of this.** That issue offers a cheap answer for "internal widgets plugin" — name the existing registry as one and give it an entry point when third parties arrive. Under this imperative that answer is no longer available: seventeen widgets, each with its own template and context, would be seventeen packages or one, and either way the locale and surface questions arrive with them. **This is not a user-visible change**, which makes it the kind of work that is easy to justify and easy to leave half-finished. The measure of it is not that the directories moved: it is that the next `phone-numbers` cannot be written inside core without a test saying so.
tiagoagueda added this to the 0.3.0 milestone 2026-09-09 10:15:51 +00:00
Author
Owner

Seven plugins, six packages, and a test that takes its list from the registry.

src/postulo/plugins/
    builtin/          schema.org and page-metadata
    email/            the notifier
    smtp/             the transport
    localstore/       the store
    europass/         the importer, and the reader it declares
    phone_numbers/    the feature

The issue named the measure and this meets it: "it is that the next phone-numbers
cannot be written inside core without a test saying so."
tests/test_plugin_surface.py now
asks registry.builtins() what Postulo ships rather than holding a list — so a built-in
added tomorrow is checked tomorrow, including the one nobody remembers to add to a list. It
fails on a plugin outside postulo.plugins, on one with no catalogues of its own, on one
that imports a model, and on one that imports anything else from Postulo without a written
reason. 33 tests.

Nothing Postulo ships touches the database now

That is the result worth having, and it came from drawing two lines the code had already
argued for.

The career record. Record's own docstring says "a career record, in Postulo's terms
rather than Europass's"
. So Postulo defines it: it moved to postulo.resume.importing
along with apply(), which resume/importers.py had already said belonged there —
"apply() stays in core and deliberately: an importer turns bytes into a record and does
not touch the database."
The plugin fills in a record it does not own, and the next
importer somebody writes fills in the same one. Two model imports gone.

The store contract. DocumentMetadata, ExternalRef and StorePlugin are what a store
author writes against, and docs/PLUGINS.md was pointing them at postulo.documents.stores
— a module the surface promise does not cover. They are part of postulo.plugins.api now,
and the local store imports them from where everybody else does.

Ownership scoping done wrong in a plugin is how one person sees another's data. The way not
to get it wrong in seven places is not to need it in seven places.

The five warnings

"As self-contained as possible" is the operative phrase. Four dependencies went away;
five remain and are recorded with what each is for — site for the notifier's from-address,
mail and destinations for the transport (#148), download_path for the store, the
identifier schemes and the record for the importer. Each is a reason to depend on Postulo,
not a failure of discipline, which is why the surface (#126) came first.

A rule nobody checks drifts back. Checked, and from the registry rather than a list.

Staging was available. Taken, over three commits: #126 the surface, #127 the catalogues,
this one the packages.

Moving a template moves a translation. Twenty-seven strings into six catalogues, each
keeping the translation it already had in all thirty-nine European languages — nothing
retranslated, nothing left English. The completeness test walks every set (#127), so a gap
would have failed rather than gone quiet.

shipped() says these are Postulo's. Unchanged. A package that ships inside the image
is not a third party and does not claim to be one.

One thing declined

Entry points. The issue asked for "its own registration through an entry point"; built-ins
still use register_builtin(). An entry point would be a slower import of a module in the
same distribution, and it would cost the ordering docs/PLUGINS.md promises — third-party
plugins tried first, Postulo's last, so a plugin somebody writes can take precedence over
ours. Entry points resolve into one list with no way to say after everything else. Nothing
else in the imperative depends on it: the manifest, the catalogues, the templates and the
surface are all in place, and the registration path is the one the locale machinery runs
through either way.

#125 is affected as the issue said: the cheap answer for widgets is gone, and whatever it
becomes now arrives with the locale and surface questions already answered.

3453 tests pass. Shipped in b3b7adf on 0.3.0, with main kept level.

Seven plugins, six packages, and a test that takes its list from the registry. ``` src/postulo/plugins/ builtin/ schema.org and page-metadata email/ the notifier smtp/ the transport localstore/ the store europass/ the importer, and the reader it declares phone_numbers/ the feature ``` **The issue named the measure and this meets it**: *"it is that the next `phone-numbers` cannot be written inside core without a test saying so."* `tests/test_plugin_surface.py` now asks `registry.builtins()` what Postulo ships rather than holding a list — so a built-in added tomorrow is checked tomorrow, including the one nobody remembers to add to a list. It fails on a plugin outside `postulo.plugins`, on one with no catalogues of its own, on one that imports a model, and on one that imports anything else from Postulo without a written reason. 33 tests. ## Nothing Postulo ships touches the database now That is the result worth having, and it came from drawing two lines the code had already argued for. **The career record.** `Record`'s own docstring says *"a career record, in Postulo's terms rather than Europass's"*. So Postulo defines it: it moved to `postulo.resume.importing` along with `apply()`, which `resume/importers.py` had already said belonged there — *"`apply()` stays in core and deliberately: an importer turns bytes into a record and does not touch the database."* The plugin fills in a record it does not own, and the next importer somebody writes fills in the same one. Two model imports gone. **The store contract.** `DocumentMetadata`, `ExternalRef` and `StorePlugin` are what a store author writes against, and `docs/PLUGINS.md` was pointing them at `postulo.documents.stores` — a module the surface promise does not cover. They are part of `postulo.plugins.api` now, and the local store imports them from where everybody else does. Ownership scoping done wrong in a plugin is how one person sees another's data. The way not to get it wrong in seven places is not to need it in seven places. ## The five warnings **"As self-contained as possible" is the operative phrase.** Four dependencies went away; five remain and are recorded with what each is for — `site` for the notifier's from-address, `mail` and `destinations` for the transport (#148), `download_path` for the store, the identifier schemes and the record for the importer. Each is a reason to depend on Postulo, not a failure of discipline, which is why the surface (#126) came first. **A rule nobody checks drifts back.** Checked, and from the registry rather than a list. **Staging was available.** Taken, over three commits: #126 the surface, #127 the catalogues, this one the packages. **Moving a template moves a translation.** Twenty-seven strings into six catalogues, each keeping the translation it already had in all thirty-nine European languages — nothing retranslated, nothing left English. The completeness test walks every set (#127), so a gap would have failed rather than gone quiet. **`shipped()` says these are Postulo's.** Unchanged. A package that ships inside the image is not a third party and does not claim to be one. ## One thing declined **Entry points.** The issue asked for "its own registration through an entry point"; built-ins still use `register_builtin()`. An entry point would be a slower import of a module in the same distribution, and it would cost the ordering `docs/PLUGINS.md` promises — third-party plugins tried first, Postulo's last, so a plugin somebody writes can take precedence over ours. Entry points resolve into one list with no way to say *after everything else*. Nothing else in the imperative depends on it: the manifest, the catalogues, the templates and the surface are all in place, and the registration path is the one the locale machinery runs through either way. #125 is affected as the issue said: the cheap answer for widgets is gone, and whatever it becomes now arrives with the locale and surface questions already answered. 3453 tests pass. Shipped in `b3b7adf` on `0.3.0`, with `main` kept level.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
Postulo/postulo#129
No description provided.