Skip to content

PLIP: Make Products.CMFEditions a Core Add-on #4273

@jensens

Description

@jensens

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.CMFPlone installs and works without CMFEditions
  • The Plone meta-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_module parameter in MigrationTool's Addon class 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 StagingRelationValue

Pattern 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

  1. setup.py — Remove "Products.CMFEditions" from install_requires

  2. profiles/dependencies/metadata.xml — Remove the
    <dependency>profile-Products.CMFEditions:CMFEditions</dependency> line

  3. factory.py — Keep CMFEditions in both getNonInstallableProducts() and
    getNonInstallableProfiles() (prevents manual install/uninstall when present,
    same as iterate)

  4. MigrationTool.py:93 — Add check_module:

    Addon(profile_id="Products.CMFEditions:CMFEditions", check_module="Products.CMFEditions"),
  5. 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_repository method calls with if 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
  6. Tests — Add conditional skips for tests that require CMFEditions:

    • testSiteAdminRole.py:98-104 — Skip CMFEditions permission assertions when not installed
    • test_zmi.py:97-140 — Skip CMFEditions tool ZMI tests when not installed
    • testMigrationTool.py — Adjust CMFEditions profile expectations

plone.app.layout

  1. setup.py — Remove "Products.CMFEditions >=1.2.2" from install_requires

  2. 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.

  3. 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

plone.restapi

  1. 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
  2. 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

  3. serializer/dxcontent.py:81 — Add None fallback to getToolByName

plone.exportimport

  1. setup.py — Move "Products.CMFEditions" from install_requires to
    extras_require = { "versioning": ["Products.CMFEditions"] }

  2. utils/content/export_helpers.py:18 and utils/content/import_helpers.py:23
    Conditional imports with HAS_CMFEDITIONS flag; skip revision export/import when absent

plone.app.contenttypes

  1. METADATA/setup — Remove plone.app.versioningbehavior from Requires-Dist

  2. FTI profiles — No change needed. Dexterity silently ignores unresolvable behaviors.
    The plone.versioning entries in Document.xml, Event.xml, etc. are harmless when
    plone.app.versioningbehavior is not installed.

Plone meta-package

  1. setup.cfg — Add to install_requires:
    Products.CMFEditions
    plone.app.versioningbehavior
    
    This ensures default Plone installs still include versioning (same as plone.app.iterate).

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_module pattern 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 in zcml: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, the portal_repository and 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:
    1. First: plone.restapi, plone.app.layout, plone.exportimport (make CMFEditions optional)
    2. Then: plone.app.contenttypes (remove versioningbehavior dep)
    3. Then: Products.CMFPlone (remove CMFEditions dep)
    4. 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

No one assigned

    Type

    Projects

    Status

    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions