Migration Phases¶
plone-codemod runs up to 8 sequential phases. Phases 1-4 and 6 are enabled by default; phases 5, 7, and 8 are opt-in.
Phase 1: Python Import Migration¶
Module: import_migrator.py (libcst-based)
Skip: --skip-python
Rewrites Python import statements and renames usage sites using libcst’s concrete syntax tree.
Capabilities:
Rewrites 129+ import paths
Renames functions at usage sites when the name changed
Splits mixed imports when names move to different modules
Preserves aliases, comments, and formatting
Handles multi-line imports correctly
# Before
from Products.CMFPlone.utils import safe_unicode, base_hasattr
from Products.CMFPlone.utils import directlyProvides
# After
from plone.base.utils import safe_text, base_hasattr
from zope.interface import directlyProvides
Files affected: *.py
Phase 2: ZCML Migration¶
Module: zcml_migrator.py
Skip: --skip-zcml
String replacement of dotted names in ZCML attributes (class=, for=, provides=, interface=, layer=, handler=, etc.).
<!-- Before -->
<browser:page for="plone.app.layout.navigation.interfaces.INavigationRoot" />
<!-- After -->
<browser:page for="plone.base.interfaces.siteroot.INavigationRoot" />
Replacements are derived from the imports section of migration_config.yaml, sorted longest-first to avoid partial matches.
Files affected: *.zcml
Phase 3: GenericSetup XML Migration¶
Module: zcml_migrator.py
Skip: --skip-xml
Updates interface references and view names in GenericSetup 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>
Files affected: *.xml in profiles/ directories
Phase 4: Page Template Migration¶
Module: pt_migrator.py
Skip: --skip-pt
Safe string replacements in page templates:
context/main_templatetocontext/@@main_template(acquisition to browser view)here/tocontext/(deprecated alias)prefs_main_templateto@@prefs_main_template
Files affected: *.pt
Phase 5: Bootstrap 3 to 5 Migration¶
Module: pt_migrator.py
Enable: --bootstrap
Opt-in because some projects intentionally keep Bootstrap 3 for parts of their UI.
17 data attribute renames (
data-toggle=todata-bs-toggle=, etc.)30+ CSS class renames (
pull-righttofloat-end,paneltocard, etc.)Plone-specific overrides (
plone-btntobtn, etc.)
<!-- Before -->
<button data-toggle="modal" class="btn btn-default pull-right">
<!-- After -->
<button data-bs-toggle="modal" class="btn btn-secondary float-end">
Files affected: *.pt, *.html
Phase 6: Audit¶
Module: semgrep_rules/plone6_deprecated.yaml
Skip: --skip-audit
Runs 35+ semgrep rules to detect issues that need manual attention. See Semgrep Audit Rules for the full list.
Requires: pip install plone-codemod[audit] (installs semgrep)
Phase 7: Namespace Package Migration¶
Module: namespace_migrator.py
Enable: --namespaces
Converts old-style namespace packages (pkg_resources / pkgutil) to PEP 420 implicit namespace packages.
Removes
__import__('pkg_resources').declare_namespace(__name__)declarationsRemoves
try/except ImportErrorwrappers around the aboveRemoves
from pkgutil import extend_path+__path__ = extend_path(...)patternsDeletes namespace-only
__init__.pyfiles (preserves mixed files)Cleans
namespace_packagesfromsetup.pyandsetup.cfg
Files affected: __init__.py files in namespace package directories, setup.py, setup.cfg
Already migrated to pyproject.toml? Phase 7 still works: it cleans the __init__.py files regardless of the packaging format. However, it does not modify pyproject.toml itself. If you have already manually migrated to pyproject.toml, you are expected to have also handled namespace package configuration there yourself (e.g., using implicit namespaces with hatchling, or [tool.setuptools.packages.find] with namespaces = true).
Phase 8: Packaging Migration¶
Module: packaging_migrator.py
Enable: --packaging
Converts setup.py / setup.cfg to a PEP 621 compliant pyproject.toml with hatchling build backend.
Parses
setup.pyusing AST (does not execute it)Parses
setup.cfgusing configparserGenerates
pyproject.tomlwithhatchlingandhatch-vcsConverts tool configs:
[flake8]/[isort]/[pycodestyle]to[tool.ruff.*][tool:pytest]to[tool.pytest.ini_options][coverage:*]to[tool.coverage.*]
Strips
setuptoolsfrom runtime dependenciesNormalizes license strings to SPDX
Merges into existing
pyproject.tomlif presentRemoves
check-manifestfrom.pre-commit-config.yamlDeletes
setup.py,setup.cfg,MANIFEST.inafter migration
Important: Run Phase 7 before Phase 8 (--namespaces --packaging) so namespace_packages is cleaned before pyproject.toml generation.
Already have a [project] section? Phase 8 detects an existing [project] section in pyproject.toml and skips the migration entirely to avoid overwriting a manually written or previously migrated configuration.
Files affected: setup.py, setup.cfg, MANIFEST.in (deleted), pyproject.toml (created/updated), .pre-commit-config.yaml