plone-codemod 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ ---
2
+ name: CI
3
+
4
+ on:
5
+ push:
6
+ branches-ignore: [main]
7
+ pull_request:
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ concurrency:
13
+ group: ci-${{ github.ref }}
14
+ cancel-in-progress: true
15
+
16
+ jobs:
17
+ qa:
18
+ uses: "./.github/workflows/qa.yaml"
19
+
20
+ tests:
21
+ uses: "./.github/workflows/tests.yaml"
@@ -0,0 +1,37 @@
1
+ ---
2
+ name: QA
3
+
4
+ on:
5
+ workflow_call:
6
+
7
+ permissions:
8
+ contents: read
9
+
10
+ jobs:
11
+ lint:
12
+ name: Ruff
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+
17
+ - uses: astral-sh/setup-uv@v6
18
+
19
+ - name: Run ruff check
20
+ run: uvx ruff check .
21
+
22
+ - name: Run ruff format check
23
+ run: uvx ruff format --check .
24
+
25
+ typecheck:
26
+ name: ty
27
+ runs-on: ubuntu-latest
28
+ steps:
29
+ - uses: actions/checkout@v4
30
+
31
+ - uses: astral-sh/setup-uv@v6
32
+
33
+ - name: Install dependencies
34
+ run: uv venv && uv pip install -e ".[dev]"
35
+
36
+ - name: Run ty type checker
37
+ run: uvx ty check --output-format github
@@ -0,0 +1,80 @@
1
+ ---
2
+ name: Build & upload PyPI package
3
+
4
+ on:
5
+ push:
6
+ branches: [main]
7
+ release:
8
+ types:
9
+ - published
10
+ workflow_dispatch:
11
+
12
+ concurrency:
13
+ group: release-${{ github.ref }}
14
+ cancel-in-progress: true
15
+
16
+ jobs:
17
+ qa:
18
+ uses: "./.github/workflows/qa.yaml"
19
+
20
+ tests:
21
+ uses: "./.github/workflows/tests.yaml"
22
+
23
+ build-package:
24
+ name: Build & verify package
25
+ needs:
26
+ - qa
27
+ - tests
28
+ runs-on: ubuntu-latest
29
+ permissions:
30
+ contents: read
31
+
32
+ steps:
33
+ - uses: actions/checkout@v4
34
+ with:
35
+ fetch-depth: 0
36
+ persist-credentials: false
37
+
38
+ - uses: hynek/build-and-inspect-python-package@v2
39
+
40
+ release-test-pypi:
41
+ name: Publish in-dev package to test.pypi.org
42
+ environment: release-test-pypi
43
+ if: github.event_name == 'push' && github.ref == 'refs/heads/main'
44
+ runs-on: ubuntu-latest
45
+ needs:
46
+ - build-package
47
+ permissions:
48
+ id-token: write
49
+
50
+ steps:
51
+ - name: Download packages built by build-and-inspect-python-package
52
+ uses: actions/download-artifact@v4
53
+ with:
54
+ name: Packages
55
+ path: dist
56
+
57
+ - name: Upload package to Test PyPI
58
+ uses: pypa/gh-action-pypi-publish@release/v1
59
+ with:
60
+ repository-url: https://test.pypi.org/legacy/
61
+
62
+ release-pypi:
63
+ name: Publish released package to pypi.org
64
+ environment: release-pypi
65
+ if: github.event.action == 'published'
66
+ runs-on: ubuntu-latest
67
+ needs:
68
+ - build-package
69
+ permissions:
70
+ id-token: write
71
+
72
+ steps:
73
+ - name: Download packages built by build-and-inspect-python-package
74
+ uses: actions/download-artifact@v4
75
+ with:
76
+ name: Packages
77
+ path: dist
78
+
79
+ - name: Upload package to PyPI
80
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,34 @@
1
+ ---
2
+ name: Tests
3
+
4
+ on:
5
+ workflow_call:
6
+
7
+ permissions:
8
+ contents: read
9
+
10
+ jobs:
11
+ test:
12
+ name: Python ${{ matrix.python-version }}
13
+ runs-on: ubuntu-latest
14
+ strategy:
15
+ fail-fast: false
16
+ matrix:
17
+ python-version: ["3.12", "3.13", "3.14"]
18
+
19
+ steps:
20
+ - uses: actions/checkout@v4
21
+
22
+ - uses: astral-sh/setup-uv@v6
23
+ with:
24
+ enable-cache: true
25
+ cache-dependency-glob: "pyproject.toml"
26
+
27
+ - name: Set up Python ${{ matrix.python-version }}
28
+ run: uv python install ${{ matrix.python-version }}
29
+
30
+ - name: Install dependencies
31
+ run: uv venv && uv pip install -e ".[dev]"
32
+
33
+ - name: Run tests
34
+ run: uv run pytest -v
@@ -0,0 +1,13 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ .pytest_cache/
5
+ *.egg-info/
6
+ dist/
7
+ build/
8
+ .libcst.codemod.yaml
9
+ .eggs/
10
+ .ruff_cache/
11
+ .coverage
12
+ htmlcov/
13
+ .venv/
@@ -0,0 +1,24 @@
1
+ repos:
2
+ - repo: https://github.com/astral-sh/ruff-pre-commit
3
+ rev: v0.15.0
4
+ hooks:
5
+ - id: ruff
6
+ args: [--fix]
7
+ - id: ruff-format
8
+
9
+ - repo: https://github.com/pre-commit/pre-commit-hooks
10
+ rev: v5.0.0
11
+ hooks:
12
+ - id: trailing-whitespace
13
+ - id: end-of-file-fixer
14
+ - id: check-yaml
15
+ - id: check-added-large-files
16
+
17
+ - repo: local
18
+ hooks:
19
+ - id: ty
20
+ name: ty type checker
21
+ entry: uvx ty check
22
+ language: system
23
+ types: [python]
24
+ pass_filenames: false
@@ -0,0 +1,274 @@
1
+ Metadata-Version: 2.4
2
+ Name: plone-codemod
3
+ Version: 0.1.0
4
+ Summary: Automated code migration tool for Plone 5.2 → 6.x upgrades
5
+ Project-URL: Homepage, https://github.com/bluedynamics/plone-codemod
6
+ Project-URL: Repository, https://github.com/bluedynamics/plone-codemod
7
+ Project-URL: Issues, https://github.com/bluedynamics/plone-codemod/issues
8
+ License-Expression: GPL-2.0-only
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Framework :: Plone
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: GNU General Public License v2 (GPLv2)
13
+ Classifier: Programming Language :: Python :: 3 :: Only
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Topic :: Software Development :: Code Generators
18
+ Requires-Python: >=3.12
19
+ Requires-Dist: libcst>=1.0
20
+ Requires-Dist: pyyaml>=6.0
21
+ Requires-Dist: semgrep
22
+ Provides-Extra: dev
23
+ Requires-Dist: coverage[toml]>=7.0; extra == 'dev'
24
+ Requires-Dist: pytest; extra == 'dev'
25
+ Requires-Dist: pytest-cov; extra == 'dev'
26
+ Description-Content-Type: text/markdown
27
+
28
+ # plone-codemod
29
+
30
+ Automated code migration tool for upgrading Plone add-ons and projects from Plone 5.2 to Plone 6.x.
31
+
32
+ Unlike simple `sed`/`find` scripts, plone-codemod uses [libcst](https://github.com/Instagram/LibCST) (a concrete syntax tree parser) to correctly handle multi-line imports, aliased imports, mixed imports, and scoped usage-site renaming.
33
+
34
+ ## What it does
35
+
36
+ **Python files** (libcst-based, AST-aware):
37
+ - Rewrites 129+ import paths (`Products.CMFPlone.*` → `plone.base.*`, etc.)
38
+ - Renames functions at usage sites (`safe_unicode()` → `safe_text()`, `getNavigationRoot()` → `get_navigation_root()`, etc.)
39
+ - Splits mixed imports when names move to different modules
40
+ - Preserves aliases, comments, and formatting
41
+
42
+ **ZCML files** (string replacement):
43
+ - Updates dotted names in `class=`, `for=`, `provides=`, `interface=` and other attributes
44
+
45
+ **GenericSetup XML** (string replacement):
46
+ - Updates interface references in `registry.xml` and profile XML
47
+ - Replaces removed view names (`folder_summary_view` → `folder_listing`, etc.)
48
+
49
+ **Page templates** (string replacement):
50
+ - `context/main_template` → `context/@@main_template` (acquisition → browser view)
51
+ - `here/` → `context/` (deprecated alias)
52
+ - `prefs_main_template` → `@@prefs_main_template`
53
+
54
+ **Bootstrap 3 → 5** (opt-in via `--bootstrap`):
55
+ - `data-toggle` → `data-bs-toggle` (and 17 other data attributes)
56
+ - CSS class renames: `pull-right` → `float-end`, `panel` → `card`, `btn-default` → `btn-secondary`, etc.
57
+ - Plone-specific overrides: `plone-btn` → `btn`, etc.
58
+
59
+ **Audit** (semgrep, optional):
60
+ - 35+ rules to detect deprecated imports, removed skin scripts, portal_properties usage, Bootstrap 3 patterns, and more
61
+ - Use in CI to prevent regressions
62
+
63
+ ## Installation
64
+
65
+ ```bash
66
+ pip install plone-codemod
67
+
68
+ # Or with uv
69
+ uv pip install plone-codemod
70
+
71
+ # Optional: for audit phase
72
+ pip install plone-codemod[audit]
73
+ ```
74
+
75
+ ## Usage
76
+
77
+ ```bash
78
+ # Preview what would change (no files modified)
79
+ plone-codemod /path/to/your/src/ --dry-run
80
+
81
+ # Apply all migrations (without Bootstrap)
82
+ plone-codemod /path/to/your/src/
83
+
84
+ # Include Bootstrap 3→5 migration
85
+ plone-codemod /path/to/your/src/ --bootstrap
86
+
87
+ # Preview Bootstrap changes
88
+ plone-codemod /path/to/your/src/ --bootstrap --dry-run
89
+
90
+ # Run only specific phases
91
+ plone-codemod /path/to/your/src/ --skip-python # ZCML + XML + PT only
92
+ plone-codemod /path/to/your/src/ --skip-zcml # Python + XML + PT only
93
+ plone-codemod /path/to/your/src/ --skip-pt # Skip page templates
94
+ plone-codemod /path/to/your/src/ --skip-audit # Skip semgrep audit
95
+
96
+ # Use a custom config
97
+ plone-codemod /path/to/your/src/ --config my_config.yaml
98
+ ```
99
+
100
+ After running, review changes with `git diff` and commit.
101
+
102
+ ## How it works
103
+
104
+ ### Phase 1: Python imports (libcst)
105
+
106
+ 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:
107
+
108
+ ```python
109
+ # Multi-line imports
110
+ from Products.CMFPlone.utils import (
111
+ safe_unicode, # → safe_text
112
+ base_hasattr, # stays, module path updated
113
+ )
114
+
115
+ # Aliased imports (alias preserved)
116
+ from Products.CMFPlone.utils import safe_unicode as su
117
+ # → from plone.base.utils import safe_text as su
118
+
119
+ # Mixed imports split when destinations differ
120
+ from Products.CMFPlone.utils import safe_unicode, directlyProvides
121
+ # → from plone.base.utils import safe_text
122
+ # → from zope.interface import directlyProvides
123
+
124
+ # Usage sites renamed only when imported from the old module
125
+ text = safe_unicode(value) # → safe_text(value)
126
+ ```
127
+
128
+ ### Phase 2: ZCML migration
129
+
130
+ String replacement of dotted names in `.zcml` files:
131
+
132
+ ```xml
133
+ <!-- Before -->
134
+ <browser:page for="plone.app.layout.navigation.interfaces.INavigationRoot" />
135
+
136
+ <!-- After -->
137
+ <browser:page for="plone.base.interfaces.siteroot.INavigationRoot" />
138
+ ```
139
+
140
+ ### Phase 3: GenericSetup XML
141
+
142
+ Updates `registry.xml` and type profile XML files:
143
+
144
+ ```xml
145
+ <!-- Before -->
146
+ <records interface="Products.CMFPlone.interfaces.controlpanel.IEditingSchema">
147
+ <property name="default_view">folder_summary_view</property>
148
+
149
+ <!-- After -->
150
+ <records interface="plone.base.interfaces.controlpanel.IEditingSchema">
151
+ <property name="default_view">folder_listing</property>
152
+ ```
153
+
154
+ ### Phase 4: Page templates
155
+
156
+ Safe automated fixes for `.pt` files:
157
+
158
+ ```xml
159
+ <!-- Before -->
160
+ <html metal:use-macro="context/main_template/macros/master">
161
+ <div tal:define="x here/title">
162
+
163
+ <!-- After -->
164
+ <html metal:use-macro="context/@@main_template/macros/master">
165
+ <div tal:define="x context/title">
166
+ ```
167
+
168
+ ### Phase 5: Bootstrap 3 → 5 (opt-in)
169
+
170
+ Only runs when `--bootstrap` is passed. Handles data attributes and CSS classes:
171
+
172
+ ```html
173
+ <!-- Before -->
174
+ <button data-toggle="modal" data-target="#m" class="btn btn-default pull-right">
175
+ <div class="panel panel-default"><div class="panel-body">...</div></div>
176
+
177
+ <!-- After -->
178
+ <button data-bs-toggle="modal" data-bs-target="#m" class="btn btn-secondary float-end">
179
+ <div class="card"><div class="card-body">...</div></div>
180
+ ```
181
+
182
+ Bootstrap migration is opt-in because some projects intentionally keep Bootstrap 3 for parts of their UI.
183
+
184
+ ### Phase 6: Audit (optional)
185
+
186
+ Runs semgrep rules to detect issues that need manual attention:
187
+
188
+ ```bash
189
+ # Standalone semgrep usage
190
+ semgrep --config semgrep_rules/ /path/to/your/src/
191
+ ```
192
+
193
+ 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.
194
+
195
+ ## Migration config
196
+
197
+ All mappings live in `migration_config.yaml`. To add a new migration rule, add an entry:
198
+
199
+ ```yaml
200
+ imports:
201
+ - old: old.module.path.OldName
202
+ new: new.module.path.NewName
203
+ ```
204
+
205
+ The tool splits on the last `.` to determine module vs name.
206
+
207
+ ### Coverage
208
+
209
+ | Category | Count |
210
+ |----------|-------|
211
+ | `Products.CMFPlone.utils` → `plone.base.utils` | 18 functions |
212
+ | `Products.CMFPlone.interfaces` → `plone.base.interfaces` | 60+ interfaces |
213
+ | Control panel interfaces | 20+ |
214
+ | TinyMCE interfaces | 5 |
215
+ | Navigation root functions | 3 |
216
+ | Syndication interfaces | 4 |
217
+ | `plone.dexterity.utils` → `plone.dexterity.schema` | 4 |
218
+ | Message factory, batch, permissions, defaultpage, i18n | 10+ |
219
+ | Special case: `ILanguageSchema` → `plone.i18n` | 1 |
220
+ | Page template patterns | 5 |
221
+ | Bootstrap data attributes | 17 |
222
+ | Bootstrap CSS class renames | 30+ |
223
+
224
+ ### What it does NOT cover (manual migration needed)
225
+
226
+ - **Archetypes removal** — AT content types must be migrated to Dexterity before upgrading
227
+ - **`getFolderContents()`** → `restrictedTraverse("@@contentlisting")()` (method call rewrite, flagged by semgrep)
228
+ - **`queryCatalog`** removal (flagged by semgrep)
229
+ - **`portal_properties`** removal (flagged by semgrep — needs registry migration)
230
+ - **Removed skin scripts** in TAL expressions (flagged by semgrep)
231
+ - **`getViewTemplateId`** deprecation (flagged by semgrep)
232
+ - **Buildout → pip/mxdev** migration (different config format)
233
+ - **Python 2 cleanup** (`six`, `__future__`, `u""` strings) — use [pyupgrade](https://github.com/asottile/pyupgrade) for this
234
+ - **Resource registry / LESS** changes (complete rewrite needed)
235
+ - **Glyphicons** → Bootstrap Icons / SVG (flagged by semgrep)
236
+ - **Dynamic imports** (`importlib.import_module("Products.CMFPlone.utils")`)
237
+
238
+ ## Development
239
+
240
+ ```bash
241
+ # Clone and install in dev mode
242
+ git clone https://github.com/bluedynamics/plone-codemod.git
243
+ cd plone-codemod
244
+ uv venv && uv pip install -e ".[dev]"
245
+
246
+ # Run tests
247
+ uv run pytest tests/ -v
248
+
249
+ # Lint
250
+ uvx ruff check .
251
+ uvx ruff format --check .
252
+ ```
253
+
254
+ ## Architecture
255
+
256
+ ```
257
+ plone-codemod/
258
+ src/plone_codemod/
259
+ cli.py # Orchestrator with --dry-run, --bootstrap
260
+ import_migrator.py # libcst codemod: Python imports + usage sites
261
+ zcml_migrator.py # ZCML + GenericSetup XML transformer
262
+ pt_migrator.py # Page template + Bootstrap migrator
263
+ migration_config.yaml # Declarative old→new mapping (YAML)
264
+ semgrep_rules/
265
+ plone6_deprecated.yaml # 35+ audit/detection rules
266
+ tests/
267
+ test_import_migrator.py # 32 tests for Python migration
268
+ test_zcml_migrator.py # 17 tests for ZCML/XML migration
269
+ test_pt_migrator.py # 24 tests for PT + Bootstrap migration
270
+ ```
271
+
272
+ ## License
273
+
274
+ GPL-2.0 — same as Plone.