-
-
Notifications
You must be signed in to change notification settings - Fork 208
Description
PLIP (Plone Improvement Proposal)
Responsible Persons
Proposer: Jens W. Klein (jensens)
Seconder:
Abstract
Products.CMFEditions is currently a hard dependency of Products.CMFPlone. It is installed
and active on every Plone site, adding significant overhead to every content creation and
modification — even when versioning is never used.
This PLIP proposes to move Products.CMFEditions from a core dependency (below CMFPlone) to
a core add-on (above CMFPlone, in the Plone meta-package) — exactly as was done with
plone.app.iterate.
After this change:
Products.CMFPloneinstalls and works without CMFEditions- The
Plonemeta-package still pulls in CMFEditions (so default installs are unchanged) - Sites that don't need versioning can exclude it
- The plone.app.iterate decoupling serves as the proven pattern
Motivation
Every IObjectAddedEvent and IObjectModifiedEvent triggers the versioning stack
(via plone.app.versioningbehavior), which performs:
- A full
pickle.dumps()+pickle.loads()clone of the object - 5 modifier passes walking the object graph
- A ZVC repository storage write creating ~180 additional persistent objects
- A transaction savepoint
Code analysis of the CMFEditions save chain shows ~8-15 extra persistent objects created
per Document version save (ShadowHistory, IOBTree, 2x ObjectData, ZVCAwareWrapper, ZVC
internals, modifier adapters). The overhead is real: a full pickle.dumps()/pickle.loads()
clone, 5 modifier passes, and a transaction savepoint — all on every content save.
Assumptions
- The plone.app.iterate decoupling pattern is proven and well-understood.
- Dexterity silently ignores behaviors it cannot resolve (so FTI profiles need no changes).
- Zope permission checks accept strings and return False gracefully for unregistered permissions.
- The
check_moduleparameter in MigrationTool'sAddonclass is a proven pattern (used by iterate, caching, multilingual, volto).
Proposal & Implementation
The Versioning Stack
Plone (meta-package, setup.cfg)
├── Products.CMFPlone ──────────── install_requires: Products.CMFEditions ← TO REMOVE
│ └── (GenericSetup profile dep on Products.CMFEditions:CMFEditions) ← TO REMOVE
│
├── plone.app.contenttypes ─────── Requires-Dist: plone.app.versioningbehavior ← TO REMOVE
│ └── FTI profiles: Document.xml, Event.xml, News_Item.xml, Link.xml
│ └── <element value="plone.versioning" /> (behavior)
│
├── plone.app.versioningbehavior ── Requires-Dist: Products.CMFEditions >2.2.9
│ ├── IVersionable behavior (changeNote, versioning_enabled)
│ ├── IVersioningSupport marker interface
│ └── Subscribers: create_initial_version_after_adding
│ create_version_on_save
│
└── Products.CMFEditions
├── portal_repository (CopyModifyMergeRepositoryTool)
├── portal_archivist (ArchivistTool)
├── portal_historiesstorage (ZVCStorageTool)
├── portal_modifier (ModifierRegistryTool)
└── portal_purgepolicy
Related but independent: Products.CMFDiffTool — provides portal_diff, registered
in CMFPlone's toolset.xml. Does NOT depend on CMFEditions. Stays in core. For now, to limit scope.
Established Patterns (plone.app.iterate Precedent)
plone.app.iterate was successfully decoupled from CMFPlone using these patterns:
Pattern 1: Conditional Import with importlib.metadata.distribution()
File: Products.CMFPlone/src/Products/CMFPlone/relationhelper.py:31-39
from importlib.metadata import distribution
from importlib.metadata import PackageNotFoundError
try:
distribution("plone.app.iterate")
except PackageNotFoundError:
HAS_ITERATE = False
else:
HAS_ITERATE = True
from plone.app.iterate.dexterity import ITERATE_RELATION_NAME
from plone.app.iterate.dexterity.relation import StagingRelationValuePattern 2: Optional Addon in MigrationTool
File: Products.CMFPlone/src/Products/CMFPlone/MigrationTool.py:104
Addon(profile_id="plone.app.iterate:default", check_module="plone.app.iterate"),The check_module parameter makes the Addon.safe() method return False when
the module is not importable, skipping the upgrade step gracefully.
Pattern 3: Dependency in Plone meta-package (not CMFPlone)
File: Plone/setup.cfg:47
plone.app.iterate
iterate is a dependency of the Plone meta-package, not of Products.CMFPlone.
This means a minimal install (just CMFPlone) doesn't include it, but the standard
Plone install does.
Per-Package Dependency Analysis
1. Products.CMFPlone
| File | Line(s) | Type | What | Details |
|---|---|---|---|---|
setup.py |
109 | HARD | install_requires | "Products.CMFEditions" |
profiles/dependencies/metadata.xml |
7 | PROFILE | GenericSetup dep | profile-Products.CMFEditions:CMFEditions |
factory.py |
62-63 | HARD | Non-installable products | "CMFEditions", "Products.CMFEditions" |
factory.py |
92 | HARD | Non-installable profiles | "Products.CMFEditions:CMFEditions" |
MigrationTool.py |
93 | HARD | ADDON_LIST | Addon(profile_id="Products.CMFEditions:CMFEditions") — no check_module |
controlpanel/browser/types.py |
121 | HARD | Tool lookup | getToolByName(self.context, "portal_repository") — no None fallback |
controlpanel/browser/types.py |
141-165 | HARD | Versioning policies | getVersionableContentTypes(), listPolicies(), etc. |
controlpanel/browser/types.py |
298-301 | HARD | Current policy | getVersionableContentTypes(), getPolicyMap() |
tests/testSiteAdminRole.py |
98-104 | TEST | Permission assertions | 7 CMFEditions permission strings |
tests/test_zmi.py |
97-140 | TEST | ZMI tool pages | Tests for portal_historiesstorage, portal_modifier, portal_repository, portal_purgepolicy |
tests/testMigrationTool.py |
201, 213-255 | TEST | Migration tests | CMFEditions profile version tracking |
Key observation: There are zero direct Python imports from Products.CMFEditions
in CMFPlone. All interaction is via getToolByName("portal_repository").
2. plone.app.layout
| File | Line(s) | Type | What | Details |
|---|---|---|---|---|
setup.py |
58 | HARD | install_requires | "Products.CMFEditions >=1.2.2" |
viewlets/content.py |
20 | HARD | Import | from Products.CMFEditions.Permissions import AccessPreviousVersions |
viewlets/content.py |
221 | SOFT | Permission string | "CMFEditions: Access previous versions" — returns False gracefully |
viewlets/content.py |
452 | SOFT | Tool lookup | getToolByName(context, "portal_repository", None) — already has None fallback |
viewlets/content.py |
464 | SOFT | Permission string | "CMFEditions: Revert to previous versions" |
viewlets/tests/test_history.py |
85, 119, 137 | TEST | Test usage | portal_repository, permission strings |
Key observation: The actual runtime code at line 452 already handles the tool being
absent (None fallback). Only the import at line 20 is a hard failure point.
3. plone.restapi
| File | Line(s) | Type | What | Details |
|---|---|---|---|---|
services/history/patch.py |
5 | HARD | Import | from Products.CMFEditions import CMFEditionsMessageFactory as _ |
services/history/patch.py |
6 | HARD | Import | from Products.CMFEditions.interfaces.IModifier import FileTooLargeToVersionError |
services/history/patch.py |
18 | HARD | Tool lookup | getToolByName(context, "portal_repository") — no None fallback |
services/history/configure.zcml |
7 | HARD | ZCML include | <include package="Products.CMFEditions" /> |
services/history/configure.zcml |
13 | HARD | ZCML permission | permission="CMFEditions.AccessPreviousVersions" |
services/history/configure.zcml |
21 | HARD | ZCML permission | permission="CMFEditions.RevertToPreviousVersions" |
services/linkintegrity/configure.zcml |
7 | HARD | ZCML include | <include package="Products.CMFEditions" /> |
services/transactions/configure.zcml |
7 | HARD | ZCML include | <include package="Products.CMFEditions" /> |
serializer/dxcontent.py |
81 | SOFT | Tool lookup | getToolByName(self.context, "portal_repository") |
tests/test_services_history.py |
142-146 | TEST | Test usage | portal_repository tool |
Key observation: plone.restapi does NOT declare CMFEditions in its setup.py — it's
an implicit dependency. The ZCML <include> directives will fail at startup without it.
4. plone.exportimport
| File | Line(s) | Type | What | Details |
|---|---|---|---|---|
setup.py |
58 | HARD | install_requires | "Products.CMFEditions" (explicit) |
utils/content/export_helpers.py |
18 | HARD | Import | from Products.CMFEditions.ZVCStorageTool import ShadowHistory |
utils/content/export_helpers.py |
259 | HARD | Tool usage | portal_repository |
utils/content/import_helpers.py |
23-25 | HARD | Import | from Products.CMFEditions.CopyModifyMergeRepositoryTool import CopyModifyMergeRepositoryTool |
utils/content/revisions.py |
30-33 | HARD | Tool usage | portal_repository |
Key observation: plone.exportimport imports specific CMFEditions classes
(ShadowHistory, CopyModifyMergeRepositoryTool), not just tool lookups.
This is needed for revision history export/import.
5. plone.app.contenttypes
| File | Line(s) | Type | What | Details |
|---|---|---|---|---|
| METADATA | 31 | HARD | Requires-Dist | plone.app.versioningbehavior |
profiles/default/types/Document.xml |
43 | PROFILE | Behavior | <element value="plone.versioning" /> |
profiles/default/types/Event.xml |
— | PROFILE | Behavior | <element value="plone.versioning" /> |
profiles/default/types/News_Item.xml |
— | PROFILE | Behavior | <element value="plone.versioning" /> |
profiles/default/types/Link.xml |
— | PROFILE | Behavior | <element value="plone.versioning" /> |
profiles/default/types/File.xml |
— | — | No behavior | Does NOT include plone.versioning |
Key observation: Dexterity silently ignores behaviors it cannot resolve. If
plone.app.versioningbehavior is not installed, the plone.versioning behavior
entries in FTI XML are simply skipped at runtime. This means the FTI profiles can
remain as-is without breaking anything.
6. plone.app.upgrade
| File | Line(s) | Type | What | Details |
|---|---|---|---|---|
v60/alphas.py |
457 | REF | Version reference | ("Products.CMFEditions:CMFEditions", "11") |
v60/alphas.py |
163 | REF | Comment | Reference to CMFEditions upgrade step |
Key observation: Only references, no hard dependency. Upgrade steps may need
a conditional check if CMFEditions is not installed on the site being upgraded.
7. Plone meta-package
| File | Line(s) | Type | What | Details |
|---|---|---|---|---|
setup.cfg |
— | — | Not listed | CMFEditions is NOT currently in Plone's install_requires (it comes transitively via CMFPlone) |
Proposed Changes Per Package
Products.CMFPlone
-
setup.py— Remove"Products.CMFEditions"frominstall_requires -
profiles/dependencies/metadata.xml— Remove the
<dependency>profile-Products.CMFEditions:CMFEditions</dependency>line -
factory.py— Keep CMFEditions in bothgetNonInstallableProducts()and
getNonInstallableProfiles()(prevents manual install/uninstall when present,
same as iterate) -
MigrationTool.py:93— Addcheck_module:Addon(profile_id="Products.CMFEditions:CMFEditions", check_module="Products.CMFEditions"),
-
controlpanel/browser/types.py— Make versioning policy management conditional:- Change
getToolByName(self.context, "portal_repository")to
getToolByName(self.context, "portal_repository", None)(2 occurrences: lines 121, 298) - Guard all
portal_repositorymethod calls withif portal_repository is not None current_versioning_policy()returns"off"when tool is absent- The form save logic skips versioning policy changes when tool is absent
- Change
-
Tests — Add conditional skips for tests that require CMFEditions:
testSiteAdminRole.py:98-104— Skip CMFEditions permission assertions when not installedtest_zmi.py:97-140— Skip CMFEditions tool ZMI tests when not installedtestMigrationTool.py— Adjust CMFEditions profile expectations
plone.app.layout
-
setup.py— Remove"Products.CMFEditions >=1.2.2"frominstall_requires -
viewlets/content.py:20— Conditional import:try: from Products.CMFEditions.Permissions import AccessPreviousVersions except ImportError: AccessPreviousVersions = "CMFEditions: Access previous versions"
The permission string fallback works because Zope permission checks accept strings.
-
Rest of the file is already safe:
- Line 452: Already uses
getToolByName(context, "portal_repository", None) - Lines 221, 464: Permission string checks return False gracefully when permission
is not registered
- Line 452: Already uses
plone.restapi
-
services/history/patch.py:5-6— Conditional imports:try: from Products.CMFEditions import CMFEditionsMessageFactory as _ from Products.CMFEditions.interfaces.IModifier import FileTooLargeToVersionError HAS_CMFEDITIONS = True except ImportError: HAS_CMFEDITIONS = False
-
ZCML files — Use
zcml:condition:<include zcml:condition="installed Products.CMFEditions" package="Products.CMFEditions" />
Apply to:
services/history/configure.zcml,services/linkintegrity/configure.zcml,
services/transactions/configure.zcml -
serializer/dxcontent.py:81— Add None fallback togetToolByName
plone.exportimport
-
setup.py— Move"Products.CMFEditions"frominstall_requiresto
extras_require = { "versioning": ["Products.CMFEditions"] } -
utils/content/export_helpers.py:18andutils/content/import_helpers.py:23—
Conditional imports withHAS_CMFEDITIONSflag; skip revision export/import when absent
plone.app.contenttypes
-
METADATA/setup — Remove
plone.app.versioningbehaviorfromRequires-Dist -
FTI profiles — No change needed. Dexterity silently ignores unresolvable behaviors.
Theplone.versioningentries in Document.xml, Event.xml, etc. are harmless when
plone.app.versioningbehavior is not installed.
Plone meta-package
setup.cfg— Add toinstall_requires:This ensures default Plone installs still include versioning (same as plone.app.iterate).Products.CMFEditions plone.app.versioningbehavior
Deliverables
| Package | Files to Change | Complexity | Notes |
|---|---|---|---|
| Products.CMFPlone | 5 code + 3 test files | Medium | Types controlpanel is most involved |
| plone.app.layout | 1 code + setup.py | Low | Most code already handles absence |
| plone.restapi | 3 code + 3 ZCML | Medium | ZCML conditions + conditional imports |
| plone.exportimport | 2 code + setup.py | Medium | Needs careful conditional logic for revisions |
| plone.app.contenttypes | setup only | Low | FTI profiles need no changes |
| Plone | setup.cfg | Trivial | Add 2 lines |
Total: ~14 files across 6 repositories
Risks
Low Risk
- FTI behavior entries: Dexterity silently ignores unknown behaviors — no breakage
- Permission strings: Zope returns False for unregistered permissions — features degrade gracefully
- MigrationTool: The
check_modulepattern is proven (used by iterate, caching, multilingual, volto)
Medium Risk
- plone.restapi ZCML: The
<include>directives will fail at startup without CMFEditions
if not wrapped inzcml:condition. Must be addressed before the CMFPlone change. - plone.exportimport: Hard imports of specific CMFEditions classes. Content export/import
with revision history will fail. Needs conditional handling. - Existing sites upgrading: Sites that have versioning data in the ZODB will keep working
as long as CMFEditions stays installed. But if someone removes CMFEditions from an existing
site, theportal_repositoryand related tools remain as broken objects. A migration step
or documentation is needed.
High Risk
- Package ordering: Changes span 6 repositories. The order of releases matters:
- First: plone.restapi, plone.app.layout, plone.exportimport (make CMFEditions optional)
- Then: plone.app.contenttypes (remove versioningbehavior dep)
- Then: Products.CMFPlone (remove CMFEditions dep)
- Finally: Plone meta-package (add CMFEditions dep here)
If CMFPlone drops the dep before downstream packages are ready, installations break.
Migration Concerns
- Sites created with CMFEditions have portal tools (
portal_repository,portal_archivist,
portal_historiesstorage,portal_modifier,portal_purgepolicy) in the ZODB.
These persist even if CMFEditions is uninstalled. A cleanup migration step would be
needed for sites that want to fully remove versioning. - Version history data in the ZVC storage would become orphaned but harmless (just wasted space).
Participants
Metadata
Metadata
Assignees
Type
Projects
Status