The four plugins that already ship in the box declare who they are #98

Closed
opened 2026-09-07 15:26:19 +00:00 by tiagoagueda · 1 comment
Owner

Observation

in this context the already existing one internal plugins must comply with this, and the
implementations of Europass import XML and JSON, SMTP and Multiple contacts per user, will
be upgraded to internal shipped plugins

The first half, and the easy half. #97 says every plugin declares a short name, a full name,
an author, a version, a description and a source link. The plugins Postulo ships must be the
first to do it, or the requirement is one this project asks of other people and not of
itself.

There are four, not two

Easy to miscount, because they are registered in three different places:

Plugin Kind Registered in
SchemaOrgSource source plugins/builtin.py -> BUILTIN_SOURCES
PageMetadataSource source plugins/builtin.py -> BUILTIN_SOURCES
EmailNotifier notifier notifications/apps.py -> register_builtin
LocalStore store documents/apps.py -> register_builtin

Between them they declare name, version, and -- for the two connected ones -- kind and
label. That is the whole of it:

class SchemaOrgSource:
    name = "schema.org"
    version = "1.0"

No full name, no description, no author, no source link. version = "1.0" has meant 1.0
since it was written and will go on meaning 1.0 through every change to the parser, so it
identifies nothing -- which matters, because base.py says the source name is "recorded
against every capture this source produced"
and the version is "so a capture can be traced
to the code that made it"
. It cannot be traced to anything.

What they should say

Author: this project. Licence: AGPL-3.0-or-later. Source link: this repository. Version:
Postulo's own, because that is the truth -- these ship with the application and change
when it does. A built-in claiming an independent version number is inventing a fact.

EmailNotifier's own docstring already argues for exactly this:

it is a plugin like any other so that nothing about it is special

The one thing that is not a rename

name is written into every capture's source field. Changing schema.org or
page-metadata orphans the history of every capture anybody has made. Raised in #97 and
repeated here because this is the issue where somebody would be tempted to do it in passing:
if the identifiers change, a data migration comes with them.

Scope

  • The four built-ins declare the full set from #97.
  • Their version follows postulo.__version__ rather than a literal.
  • A test that walks every built-in and fails on a missing field -- the point being that a
    fifth built-in added later cannot skip it.

Classification

Enhancement. Small, and blocked only by #97 settling what the fields are.

## Observation > in this context the already existing one internal plugins must comply with this, and the > implementations of Europass import XML and JSON, SMTP and Multiple contacts per user, will > be upgraded to internal shipped plugins The first half, and the easy half. #97 says every plugin declares a short name, a full name, an author, a version, a description and a source link. The plugins Postulo ships must be the first to do it, or the requirement is one this project asks of other people and not of itself. ## There are four, not two Easy to miscount, because they are registered in three different places: | Plugin | Kind | Registered in | | --- | --- | --- | | `SchemaOrgSource` | source | `plugins/builtin.py` -> `BUILTIN_SOURCES` | | `PageMetadataSource` | source | `plugins/builtin.py` -> `BUILTIN_SOURCES` | | `EmailNotifier` | notifier | `notifications/apps.py` -> `register_builtin` | | `LocalStore` | store | `documents/apps.py` -> `register_builtin` | Between them they declare `name`, `version`, and -- for the two connected ones -- `kind` and `label`. That is the whole of it: ```python class SchemaOrgSource: name = "schema.org" version = "1.0" ``` No full name, no description, no author, no source link. `version = "1.0"` has meant 1.0 since it was written and will go on meaning 1.0 through every change to the parser, so it identifies nothing -- which matters, because `base.py` says the source name is *"recorded against every capture this source produced"* and the version is *"so a capture can be traced to the code that made it"*. It cannot be traced to anything. ## What they should say Author: this project. Licence: AGPL-3.0-or-later. Source link: this repository. Version: **Postulo's own**, because that is the truth -- these ship with the application and change when it does. A built-in claiming an independent version number is inventing a fact. `EmailNotifier`'s own docstring already argues for exactly this: > it is a plugin like any other so that nothing about it is special ## The one thing that is not a rename `name` is written into every capture's `source` field. Changing `schema.org` or `page-metadata` orphans the history of every capture anybody has made. Raised in #97 and repeated here because this is the issue where somebody would be tempted to do it in passing: if the identifiers change, a data migration comes with them. ## Scope - The four built-ins declare the full set from #97. - Their version follows `postulo.__version__` rather than a literal. - A test that walks every built-in and fails on a missing field -- the point being that a fifth built-in added later cannot skip it. ## Classification Enhancement. Small, and blocked only by #97 settling what the fields are.
tiagoagueda added this to the 0.3.0 milestone 2026-09-07 15:26:19 +00:00
Author
Owner

Done in 81c894b, built around your suggestion: the facts live in one manifest rather than one attribute each.

Your instinct was right for a reason the issue had not spotted. A loose attribute per fact can never be made required — runtime_checkable protocols check data members, and registry.py drops anything failing isinstance, so adding label to SourcePlugin would silently unload every source anybody has already written. That is why #97 left label and description optional and read them through helpers, and it does not scale: every new fact is a new optional attribute for every plugin author to learn about. One optional attribute carrying any number of facts has neither problem, and a field added later costs a plugin that has not heard of it nothing.

@declares(
    Manifest(
        name="acme-board", label="ACME Board", version="1.4.0", kind="source",
        description="...", author="First Last <first.last@example.org>",
        licence="AGPL-3.0-or-later", source_url="https://example.org/...",
        logo="https://example.org/.../logo.png",
    )
)
class AcmeBoardSource: ...

@declares also sets name, version, kind, label and description on the class from the manifest, so the identifier the registry keys on and the one in the manifest cannot disagree — the day they do is the day a capture is filed against a plugin that does not exist. All of it is optional; a plugin that declares nothing still loads and shows its identifier where a name goes.

manifest_of() looks in three places, earlier winning: the manifest, the loose attributes, and the wheel the plugin was installed from. That third one is what makes "one place to look" true rather than aspirational — a third-party plugin that never heard of manifests still has an author and a licence in its packaging, and #97 already taught Postulo to read them. label_of and description_of survive as shorthands over it, so no call site had to change.

Two corrections to the issue.

There are six built-ins, not four — it was written before Europass (#99) and SMTP (#104) landed. The test walks registry.builtins() rather than naming them, which is the point: a seventh cannot skip the declaration, and miscounting by hand is exactly what happened here. There is a second test asserting the set of six, so losing one to a bad import is a failure rather than a quiet absence.

And the identity is now shown, which the scope did not ask for but #97's whole complaint required: Server settings → Plugins prints version, author, licence and a Source link for every plugin, built-in or installed, on one line. Declaring facts nothing renders would have been the same bug in a new place. The mail transport is the one plugin that page does not list — transports are not governed per person (#104) — so the same line went on the Email page beside it.

logo is in the manifest with nothing rendering it. Deliberate: that is #106's work, and having the slot means #106 becomes "ship the mark and point at it" rather than "invent somewhere to put it".

The identifiers did not move. schema.org and page-metadata are in the source field of every capture anybody has made; a test pins them, in the file somebody would be editing when they were tempted.

docs/PLUGINS.md's Saying who you are is rewritten around the manifest, including why none of it can be required. 10 tests in tests/test_plugin_manifests.py; 4 new strings, translated into all 24 catalogues and flagged draft.

Done in 81c894b, built around your suggestion: the facts live in one **manifest** rather than one attribute each. **Your instinct was right for a reason the issue had not spotted.** A loose attribute per fact can never be made *required* — `runtime_checkable` protocols check data members, and `registry.py` drops anything failing `isinstance`, so adding `label` to `SourcePlugin` would silently unload every source anybody has already written. That is why #97 left `label` and `description` optional and read them through helpers, and it does not scale: every new fact is a new optional attribute for every plugin author to learn about. One optional attribute carrying any number of facts has neither problem, and a field added later costs a plugin that has not heard of it nothing. ```python @declares( Manifest( name="acme-board", label="ACME Board", version="1.4.0", kind="source", description="...", author="First Last <first.last@example.org>", licence="AGPL-3.0-or-later", source_url="https://example.org/...", logo="https://example.org/.../logo.png", ) ) class AcmeBoardSource: ... ``` `@declares` also sets `name`, `version`, `kind`, `label` and `description` on the class **from** the manifest, so the identifier the registry keys on and the one in the manifest cannot disagree — the day they do is the day a capture is filed against a plugin that does not exist. All of it is optional; a plugin that declares nothing still loads and shows its identifier where a name goes. **`manifest_of()` looks in three places**, earlier winning: the manifest, the loose attributes, and **the wheel the plugin was installed from**. That third one is what makes "one place to look" true rather than aspirational — a third-party plugin that never heard of manifests still has an author and a licence in its packaging, and #97 already taught Postulo to read them. `label_of` and `description_of` survive as shorthands over it, so no call site had to change. **Two corrections to the issue.** There are **six** built-ins, not four — it was written before Europass (#99) and SMTP (#104) landed. The test walks `registry.builtins()` rather than naming them, which is the point: a seventh cannot skip the declaration, and miscounting by hand is exactly what happened here. There is a second test asserting the set of six, so losing one to a bad import is a failure rather than a quiet absence. And the identity is now **shown**, which the scope did not ask for but #97's whole complaint required: *Server settings → Plugins* prints version, author, licence and a Source link for every plugin, built-in or installed, on one line. Declaring facts nothing renders would have been the same bug in a new place. The mail transport is the one plugin that page does not list — transports are not governed per person (#104) — so the same line went on the Email page beside it. **`logo` is in the manifest with nothing rendering it.** Deliberate: that is #106's work, and having the slot means #106 becomes "ship the mark and point at it" rather than "invent somewhere to put it". The identifiers did not move. `schema.org` and `page-metadata` are in the `source` field of every capture anybody has made; a test pins them, in the file somebody would be editing when they were tempted. `docs/PLUGINS.md`'s *Saying who you are* is rewritten around the manifest, including why none of it can be required. 10 tests in `tests/test_plugin_manifests.py`; 4 new strings, translated into all 24 catalogues and flagged `draft`.
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#98
No description provided.