Skip to main content

Automated code migration tool for Plone 5.2 → 6.x upgrades

Project description

plone-codemod

Automated code migration tool for upgrading Plone add-ons and projects from Plone 5.2 to Plone 6.x.

Unlike simple sed/find scripts, plone-codemod uses libcst (a concrete syntax tree parser) to correctly handle multi-line imports, aliased imports, mixed imports, and scoped usage-site renaming.

What it does

Python files (libcst-based, AST-aware):

  • Rewrites 129+ import paths (Products.CMFPlone.*plone.base.*, etc.)
  • Renames functions at usage sites (safe_unicode()safe_text(), getNavigationRoot()get_navigation_root(), etc.)
  • Splits mixed imports when names move to different modules
  • Preserves aliases, comments, and formatting

ZCML files (string replacement):

  • Updates dotted names in class=, for=, provides=, interface= and other attributes

GenericSetup XML (string replacement):

  • Updates interface references in registry.xml and profile XML
  • Replaces removed view names (folder_summary_viewfolder_listing, etc.)

Page templates (string replacement):

  • context/main_templatecontext/@@main_template (acquisition → browser view)
  • here/context/ (deprecated alias)
  • prefs_main_template@@prefs_main_template

Bootstrap 3 → 5 (opt-in via --bootstrap):

  • data-toggledata-bs-toggle (and 17 other data attributes)
  • CSS class renames: pull-rightfloat-end, panelcard, btn-defaultbtn-secondary, etc.
  • Plone-specific overrides: plone-btnbtn, etc.

Audit (semgrep, optional):

  • 35+ rules to detect deprecated imports, removed skin scripts, portal_properties usage, Bootstrap 3 patterns, and more
  • Use in CI to prevent regressions

Namespace packages → PEP 420 (opt-in via --namespaces):

  • Removes __import__('pkg_resources').declare_namespace(__name__) declarations
  • Removes pkgutil.extend_path declarations
  • Deletes namespace-only __init__.py files (or edits them if they contain other code)
  • Cleans namespace_packages from setup.py and setup.cfg

setup.py → pyproject.toml (opt-in via --packaging):

  • Parses setup.py (AST-based) and setup.cfg (configparser-based)
  • Generates PEP 621 compliant pyproject.toml with hatchling build backend
  • Converts tool configs: [flake8]/[isort]/[pycodestyle][tool.ruff.*], [tool:pytest][tool.pytest.ini_options], coverage sections, etc.
  • Strips setuptools from runtime dependencies, normalizes license strings to SPDX
  • Merges into existing pyproject.toml if present (preserves [tool.ruff] etc.)
  • Deletes setup.py, setup.cfg, MANIFEST.in after migration

Installation

pip install plone-codemod

# Or with uv
uv pip install plone-codemod

# Optional: for audit phase
pip install plone-codemod[audit]

Usage

# Preview what would change (no files modified)
plone-codemod /path/to/your/src/ --dry-run

# Apply all migrations (without Bootstrap)
plone-codemod /path/to/your/src/

# Include Bootstrap 3→5 migration
plone-codemod /path/to/your/src/ --bootstrap

# Preview Bootstrap changes
plone-codemod /path/to/your/src/ --bootstrap --dry-run

# Run only specific phases
plone-codemod /path/to/your/src/ --skip-python     # ZCML + XML + PT only
plone-codemod /path/to/your/src/ --skip-zcml        # Python + XML + PT only
plone-codemod /path/to/your/src/ --skip-pt          # Skip page templates
plone-codemod /path/to/your/src/ --skip-audit       # Skip semgrep audit

# Use a custom config
plone-codemod /path/to/your/src/ --config my_config.yaml

# Packaging modernization (opt-in)
plone-codemod /path/to/your/src/ --namespaces                 # PEP 420 namespace migration
plone-codemod /path/to/your/src/ --packaging                  # setup.py → pyproject.toml
plone-codemod /path/to/your/src/ --namespaces --packaging     # Both (recommended)
plone-codemod /path/to/your/src/ --packaging --project-dir .  # Explicit project root

After running, review changes with git diff and commit.

How it works

Phase 1: Python imports (libcst)

The codemod reads migration_config.yaml and rewrites import statements using libcst's concrete syntax tree. This means it correctly handles cases that sed cannot:

# Multi-line imports
from Products.CMFPlone.utils import (
    safe_unicode,      # → safe_text
    base_hasattr,      # stays, module path updated
)

# Aliased imports (alias preserved)
from Products.CMFPlone.utils import safe_unicode as su
# → from plone.base.utils import safe_text as su

# Mixed imports split when destinations differ
from Products.CMFPlone.utils import safe_unicode, directlyProvides
# → from plone.base.utils import safe_text
# → from zope.interface import directlyProvides

# Usage sites renamed only when imported from the old module
text = safe_unicode(value)  # → safe_text(value)

Phase 2: ZCML migration

String replacement of dotted names in .zcml files:

<!-- Before -->
<browser:page for="plone.app.layout.navigation.interfaces.INavigationRoot" />

<!-- After -->
<browser:page for="plone.base.interfaces.siteroot.INavigationRoot" />

Phase 3: GenericSetup XML

Updates registry.xml and type profile XML files:

<!-- Before -->
<records interface="Products.CMFPlone.interfaces.controlpanel.IEditingSchema">
<property name="default_view">folder_summary_view</property>

<!-- After -->
<records interface="plone.base.interfaces.controlpanel.IEditingSchema">
<property name="default_view">folder_listing</property>

Phase 4: Page templates

Safe automated fixes for .pt files:

<!-- Before -->
<html metal:use-macro="context/main_template/macros/master">
<div tal:define="x here/title">

<!-- After -->
<html metal:use-macro="context/@@main_template/macros/master">
<div tal:define="x context/title">

Phase 5: Bootstrap 3 → 5 (opt-in)

Only runs when --bootstrap is passed. Handles data attributes and CSS classes:

<!-- Before -->
<button data-toggle="modal" data-target="#m" class="btn btn-default pull-right">
<div class="panel panel-default"><div class="panel-body">...</div></div>

<!-- After -->
<button data-bs-toggle="modal" data-bs-target="#m" class="btn btn-secondary float-end">
<div class="card"><div class="card-body">...</div></div>

Bootstrap migration is opt-in because some projects intentionally keep Bootstrap 3 for parts of their UI.

Phase 7: Namespace packages → PEP 420 (opt-in)

Only runs when --namespaces is passed. Converts old-style namespace packages to PEP 420 implicit namespace packages:

# Before: src/plone/__init__.py
__import__('pkg_resources').declare_namespace(__name__)

# After: src/plone/__init__.py is DELETED (PEP 420 — no __init__.py needed)

Handles:

  • __import__('pkg_resources').declare_namespace(__name__) (pkg_resources style)
  • try/except ImportError wrappers around the above
  • from pkgutil import extend_path + __path__ = extend_path(...) (pkgutil style)
  • Nested namespaces (e.g., both plone/ and plone/app/)
  • Mixed files (namespace declaration removed, other code preserved)
  • Cleans namespace_packages from setup.py and setup.cfg

Phase 8: setup.py → pyproject.toml (opt-in)

Only runs when --packaging is passed. Converts setup.py/setup.cfg to a PEP 621 compliant pyproject.toml with hatchling build backend:

# Generated pyproject.toml
[build-system]
requires = ["hatchling", "hatch-vcs"]
build-backend = "hatchling.build"

[project]
name = "plone.app.something"
dynamic = ["version"]
description = "A Plone addon"
readme = "README.rst"
license = "GPL-2.0-only"
requires-python = ">=3.8"
dependencies = ["plone.api>=2.0", "zope.interface"]

[project.entry-points."z3c.autoinclude.plugin"]
target = "plone"

[tool.hatch.build.targets.wheel]
packages = ["src/plone"]

[tool.hatch.version]
source = "vcs"

Tool config conversion (from setup.cfg):

  • [flake8][tool.ruff.lint]
  • [isort][tool.ruff.lint.isort]
  • [pycodestyle]/[pep8][tool.ruff.lint]
  • [tool:pytest][tool.pytest.ini_options]
  • [coverage:run]/[coverage:report][tool.coverage.*]
  • [bdist_wheel] — dropped (PEP 517 handles this)

Use --project-dir to specify the project root if it's not the parent of source_dir.

Phase 6: Audit (optional)

Runs semgrep rules to detect issues that need manual attention:

# Standalone semgrep usage
semgrep --config semgrep_rules/ /path/to/your/src/

Detects: deprecated imports, removed skin scripts (queryCatalog, getFolderContents, pretty_title_or_id), portal_properties usage, checkPermission builtin in templates, getIcon, normalizeString, glyphicons, Bootstrap 3 patterns, and more.

Migration config

All mappings live in migration_config.yaml. To add a new migration rule, add an entry:

imports:
  - old: old.module.path.OldName
    new: new.module.path.NewName

The tool splits on the last . to determine module vs name.

Coverage

Category Count
Products.CMFPlone.utilsplone.base.utils 18 functions
Products.CMFPlone.interfacesplone.base.interfaces 60+ interfaces
Control panel interfaces 20+
TinyMCE interfaces 5
Navigation root functions 3
Syndication interfaces 4
plone.dexterity.utilsplone.dexterity.schema 4
Message factory, batch, permissions, defaultpage, i18n 10+
Special case: ILanguageSchemaplone.i18n 1
Page template patterns 5
Bootstrap data attributes 17
Bootstrap CSS class renames 30+

What it does NOT cover (manual migration needed)

  • Archetypes removal — AT content types must be migrated to Dexterity before upgrading
  • getFolderContents()restrictedTraverse("@@contentlisting")() (method call rewrite, flagged by semgrep)
  • queryCatalog removal (flagged by semgrep)
  • portal_properties removal (flagged by semgrep — needs registry migration)
  • Removed skin scripts in TAL expressions (flagged by semgrep)
  • getViewTemplateId deprecation (flagged by semgrep)
  • Buildout → pip/mxdev migration (different config format, use mxdev)
  • Python 2 cleanup (six, __future__, u"" strings) — use pyupgrade for this
  • Resource registry / LESS changes (complete rewrite needed)
  • Glyphicons → Bootstrap Icons / SVG (flagged by semgrep)
  • Dynamic imports (importlib.import_module("Products.CMFPlone.utils"))

Development

# Clone and install in dev mode
git clone https://github.com/bluedynamics/plone-codemod.git
cd plone-codemod
uv venv && uv pip install -e ".[dev]"

# Run tests
uv run pytest tests/ -v

# Lint
uvx ruff check .
uvx ruff format --check .

Architecture

plone-codemod/
  src/plone_codemod/
    cli.py                       # Orchestrator with all phases and CLI flags
    import_migrator.py           # libcst codemod: Python imports + usage sites
    zcml_migrator.py             # ZCML + GenericSetup XML transformer
    pt_migrator.py               # Page template + Bootstrap migrator
    namespace_migrator.py        # PEP 420 namespace package migration
    packaging_migrator.py        # setup.py → pyproject.toml migration
    migration_config.yaml        # Declarative old→new mapping (YAML)
    semgrep_rules/
      plone6_deprecated.yaml     # 35+ audit/detection rules
  tests/
    test_import_migrator.py      # 32 tests for Python migration
    test_zcml_migrator.py        # 17 tests for ZCML/XML migration
    test_pt_migrator.py          # 24 tests for PT + Bootstrap migration
    test_namespace_migrator.py   # 47 tests for namespace migration
    test_packaging_migrator.py   # 48 tests for packaging migration

Source Code and Contributions

The source code is managed in a Git repository, with its main branches hosted on GitHub. Issues can be reported there too.

We'd be happy to see many forks and pull requests to make this tool even better. We welcome AI-assisted contributions, but expect every contributor to fully understand and be able to explain the code they submit. Please don't send bulk auto-generated pull requests.

Maintainers are Jens Klein, Johannes Raggam and the BlueDynamics Alliance developer team. We appreciate any contribution and if a release on PyPI is needed, please just contact one of us. We also offer commercial support if any training, coaching, integration or adaptations are needed.

License

GPL-2.0 — same as Plone.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

plone_codemod-1.0.0a3.tar.gz (45.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

plone_codemod-1.0.0a3-py3-none-any.whl (37.1 kB view details)

Uploaded Python 3

File details

Details for the file plone_codemod-1.0.0a3.tar.gz.

File metadata

  • Download URL: plone_codemod-1.0.0a3.tar.gz
  • Upload date:
  • Size: 45.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for plone_codemod-1.0.0a3.tar.gz
Algorithm Hash digest
SHA256 1b442e81cd0853281212df9c19c9c65650f27816ec44977a26d1cf92bd2c9581
MD5 956bfe767dc349a414de7c50533a6c5c
BLAKE2b-256 f89366357a3d644161d2941503dfe22ce708581787776aee5e000aa5de43057d

See more details on using hashes here.

Provenance

The following attestation bundles were made for plone_codemod-1.0.0a3.tar.gz:

Publisher: release.yaml on bluedynamics/plone-codemod

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file plone_codemod-1.0.0a3-py3-none-any.whl.

File metadata

File hashes

Hashes for plone_codemod-1.0.0a3-py3-none-any.whl
Algorithm Hash digest
SHA256 3889ad2dd18c626ae45876a07aba56af0b005b9bf3d09120f808403e3d9b4a1c
MD5 51912416ed84f8b96f26ea20ab738edc
BLAKE2b-256 4f4032cabd3ae09a163a9e88dbc93e107b7266f3305bf5fcd224ca596089d0ef

See more details on using hashes here.

Provenance

The following attestation bundles were made for plone_codemod-1.0.0a3-py3-none-any.whl:

Publisher: release.yaml on bluedynamics/plone-codemod

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page