Say what a plugin may import, so self-contained can be checked #126
Labels
No labels
accessibility
authentication
breaking change
bug
documentation
enhancement
interface
internationalisation
observability
security
tier
1
tier
2
tier
3
tier/4
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Blocks
#151 SMTP that Google and Microsoft will still accept
Postulo/postulo
Reference
Postulo/postulo#126
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Observation
First of three prerequisites. "Do not depend on the core" cannot be enforced, or even
checked, until there is a written answer to depend on what, then.
What exists
docs/PLUGINS.mddefines a contract per kind, and every one of them is narrow anddata-shaped:
sourceJobPostingDataimporternotifier/store/syncConnectionand a payloadtransportfeatureFor those, independence is nearly free: a source imports
JobPostingDataand touches nomodel.
plugins/builtin.pyproves it — zero imports frompostulo.The rest do not have that shape.
EmailNotifier,LocalStore,EuropassImporterandPhoneNumbersFeatureeach import frompostulo, and the last of them is barely a module:its behaviour lives in
core/models.py,core/phone_numbers.py, two forms, a templatepartial and two views. A widget plugin (#125) would be the same shape again — it needs a
template, a context, and querysets scoped with
for_user().There is no declared surface for any of that. A plugin needing a model, a form, a
template or ownership scoping reaches into
postulo.coreand hopes, and nothing tells itwhich of those imports are a contract and which are this month's internals.
What this asks for
The list of what a plugin may import, written down and enforced, so that "self-contained"
becomes a property something can fail.
Worth being careful about
Total independence is not the goal and cannot be. A plugin that holds one person's data
must scope it with
for_user(), or it breaks the project's first promise; one that rendersmust extend the base template or it renders unstyled; one that redirects must go through
safe_next(). Each of those is a reason to depend on Postulo, and the imperative isserved by making them a small, named, stable set — not by pretending they are avoidable.
The candidates, roughly in order of how obviously they belong:
OwnedModelandOwnedQuerySet.for_user;safe_next; theManifest/@declaresmachinery; the fieldspecs a connection form is built from; the template blocks a page may extend; and whatever
a widget is handed. Everything else stays private.
A surface is a promise about breakage. Once
postulo.plugins.apiexists, changing itbreaks third-party plugins between releases, which is exactly the cost the project has
avoided so far by having no such surface. Whether that promise is made now, at 0.3.0, or
deferred until third parties actually exist, is the decision inside this issue.
Enforcement is what makes it real. A rule nobody checks drifts back in a release.
tests/test_plugin_manifests.pyalready walks every built-in and asserts what it declares;the same shape of test can walk a built-in package's imports and fail on one that reaches
past the surface. Without that, this issue produces a document and no change.
The stateless kinds are already there. Whatever surface is written, the two built-in
sources satisfy it today, which makes them the check that the surface is not so wide as to
be meaningless.
postulo.plugins.apiis the surface, andtests/test_plugin_surface.pyis what makes it real.The decision the issue named — now or deferred — is now. Having no surface was free, and
it stops being free the moment #129 moves shipped plugins into their own packages: a package
outside
src/postulohas to know what it may import. Deferring means either deferring thatwork or setting a boundary by accident, which is the worse of the two. From here, a change to
any name in the surface is a
### ⚠️ Deprecatedentry first and a### 🗑️ Removedentrylater, never a silent rename.
The candidates the issue listed are all in, and the four that carry the argument are
tested individually rather than counted, because each is a promise the project makes rather
than a convenience:
OwnedModel/OwnedQuerySet(or one person sees another's data),safe_next(or a plugin bounces somebody off the instance),client(or a plugin dials wherethe server should not), and
access_tokenfrom #150 (or a plugin keeps a token instead ofrefreshing it). Plus the manifest machinery, the field specs, the protocols, the import
refusals, and the transport mediums.
Enforcement reads the source rather than importing it, which matters more than it sounds:
every remaining dependency in this codebase is a lazy
from postulo.core import siteinsidea method, and an importing check would have found none of them.
REACHING_PASTis the map of what #129 has left to move, not a list of exemptions — fiveentries across three plugins, each saying what the plugin actually needs:
site, for the instance's name and from-addressmailanddestinationsabsolute_urlPersonIdentifier, andphone_numbersA stale entry fails the test too, because one nobody removed hides the next real
dependency.
And the surface is checked for not being meaningless, using exactly the plugins the issue
suggested: the two built-in sources import nothing from Postulo whatsoever, and the
telephone-numbers feature imports only the surface. Both are asserted.
What a dashboard widget is handed (#125) and what a template may extend are deliberately left
out and said to be left out, in the module docstring and in
docs/PLUGINS.md.18 tests, and a section in
docs/PLUGINS.mdfor plugin authors. No new user-facing strings —this is a contract, not an interface.
Shipped in
07700a6on0.3.0, withmainkept level. #129 and #151 both wanted this.