Plugin installer: upgrading a plugin drops the constraint on Postulo's own packages it was installed with #598

Closed
opened 2026-10-01 10:48:10 +00:00 by tiagoagueda · 0 comments
Owner

Found on 2026-09-29 in a read-only review of postulo at 95348908d, reading pin_owners and constraints, running uv pip install --target offline into a scratch folder, and running the constraint functions against a temporary plugins directory.

Checked again on 2026-10-01 against main at 0a5cad587: still present. The code is now at plugins/installing.py:281-304 (pin_owners; the pop the fix speaks of, formerly lines 474-476, is at :300-302), plugins/installing.py:307-328 (constraints), plugins/installing.py:351-390 (conflicts_with_core) and plugins/installing.py:783-790 (where the copies are recorded); the site.addsitedir call moved to plugins/record.py:89 (activate, #248).

uv pip install --target installs a plugin's whole dependency tree into the plugins directory, including copies of packages Postulo already has (an offline run for httpx laid down httpx, httpcore, anyio, h11, idna, certifi and typing_extensions, pinned to Postulo's versions by the constraint file). install_wheel records those copies as the plugin's dependencies. pin_owners() cannot tell Postulo's pins from those copies, so on that plugin's next upgrade it drops the constraint on Postulo's own packages, and the promise in the module docstring that "a plugin can never change the version of something Postulo itself depends on. The refusal names the package" does not hold. Related to #246 (closed), which built the ownership table.

  • Where: plugins/installing.py:455-478 (pin_owners: every entry of metadata.distributions(), the plugins directory included, becomes owner "", and lines 474-476 pop any package the plugin being installed brought whose owner is ""), plugins/installing.py:481-502 (constraints), plugins/installing.py:525-564 (conflicts_with_core), and plugins/installing.py:957-964 (where the copies are recorded).
  • What happens: postulo-dav and postulo-paperless both declare httpx>=0.28. Installing either records httpx==0.28.1 (and the rest of the tree) as its dependencies. Checked in the snapshot: before the install constraints(exclude="plug-a") held httpx==0.28.1; after it, it held no httpx pin, conflicts_with_core for a plug-a 1.1 needing httpx==0.30.0 returned nothing, and for a different plugin needing it returned "httpx: needs 0.30.0, and plug-a has 0.28.1." So the upgrade is not refused; the installer puts the newer httpx in the plugins directory, which site.addsitedir appends after site-packages, so both the import check and every worker go on loading Postulo's older copy. The plugin fails later, at the first call that needs the newer version, while the record and the page say the newer one is installed. For every other plugin the refusal names the first plugin instead of Postulo.
  • Why it matters: an upgrade that should have been refused with a clear reason installs "successfully" and breaks at run time, and refusals blame the wrong owner.
  • Fix: build Postulo's own pins only from distributions that live outside plugins_dir() (compare distribution.locate_file("") with the directory), never drop those for the plugin being installed, and limit the pop at lines 474-476 to packages that exist only in the plugins directory. Optionally leave out of the record, or avoid installing, packages the core environment already satisfies.

Done when

  • tests/test_plugin_rollback.py has a test that, after installing a plugin whose recorded dependencies include a package Postulo itself provides, constraints(exclude=<that plugin>) still pins Postulo's version and conflicts_with_core refuses an upgrade needing another version with the "Postulo has" wording.
Found on 2026-09-29 in a read-only review of postulo at 95348908d, reading `pin_owners` and `constraints`, running `uv pip install --target` offline into a scratch folder, and running the constraint functions against a temporary plugins directory. Checked again on 2026-10-01 against `main` at `0a5cad587`: still present. The code is now at `plugins/installing.py:281-304` (`pin_owners`; the pop the fix speaks of, formerly lines 474-476, is at :300-302), `plugins/installing.py:307-328` (`constraints`), `plugins/installing.py:351-390` (`conflicts_with_core`) and `plugins/installing.py:783-790` (where the copies are recorded); the `site.addsitedir` call moved to `plugins/record.py:89` (`activate`, #248). `uv pip install --target` installs a plugin's whole dependency tree into the plugins directory, including copies of packages Postulo already has (an offline run for `httpx` laid down `httpx`, `httpcore`, `anyio`, `h11`, `idna`, `certifi` and `typing_extensions`, pinned to Postulo's versions by the constraint file). `install_wheel` records those copies as the plugin's dependencies. `pin_owners()` cannot tell Postulo's pins from those copies, so on that plugin's next upgrade it drops the constraint on Postulo's own packages, and the promise in the module docstring that "a plugin can never change the version of something Postulo itself depends on. The refusal names the package" does not hold. Related to #246 (closed), which built the ownership table. - **Where:** `plugins/installing.py:455-478` (`pin_owners`: every entry of `metadata.distributions()`, the plugins directory included, becomes owner `""`, and lines 474-476 pop any package the plugin being installed brought whose owner is `""`), `plugins/installing.py:481-502` (`constraints`), `plugins/installing.py:525-564` (`conflicts_with_core`), and `plugins/installing.py:957-964` (where the copies are recorded). - **What happens:** postulo-dav and postulo-paperless both declare `httpx>=0.28`. Installing either records `httpx==0.28.1` (and the rest of the tree) as its dependencies. Checked in the snapshot: before the install `constraints(exclude="plug-a")` held `httpx==0.28.1`; after it, it held no `httpx` pin, `conflicts_with_core` for a plug-a 1.1 needing `httpx==0.30.0` returned nothing, and for a different plugin needing it returned "httpx: needs 0.30.0, and plug-a has 0.28.1." So the upgrade is not refused; the installer puts the newer `httpx` in the plugins directory, which `site.addsitedir` appends after site-packages, so both the import check and every worker go on loading Postulo's older copy. The plugin fails later, at the first call that needs the newer version, while the record and the page say the newer one is installed. For every other plugin the refusal names the first plugin instead of Postulo. - **Why it matters:** an upgrade that should have been refused with a clear reason installs "successfully" and breaks at run time, and refusals blame the wrong owner. - **Fix:** build Postulo's own pins only from distributions that live outside `plugins_dir()` (compare `distribution.locate_file("")` with the directory), never drop those for the plugin being installed, and limit the pop at lines 474-476 to packages that exist only in the plugins directory. Optionally leave out of the record, or avoid installing, packages the core environment already satisfies. ## Done when - `tests/test_plugin_rollback.py` has a test that, after installing a plugin whose recorded dependencies include a package Postulo itself provides, `constraints(exclude=<that plugin>)` still pins Postulo's version and `conflicts_with_core` refuses an upgrade needing another version with the "Postulo has" wording.
tiagoagueda added this to the 0.5.0 milestone 2026-10-01 10:48:10 +00:00
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.

Dependencies

No dependencies set

Reference
Postulo/postulo#598
No description provided.