Commit ef603c1
Python: Pipfile support and dependency recipe rework (#7521)
* Added pipenv lockfiles
* Added pip parsing
* Added pip parsing
* Added pip parsing
* Plan: Python dependency recipe Accumulator rework
* Add PyProjectHelper.regenerateLockContent dispatcher
* AddDependency: move per-project state to Accumulator and run regen in generate()
Refactors AddDependency to use a richer Accumulator with per-project
ProjectState objects that capture lock file paths, lock content, and
dependency-match flags during scanning. Dependency modification and
lock regeneration now run in the visitor on the live (chain-modified)
tree so that composite recipes correctly chain their modifications.
Replaces ExecutionContext-based lock state with accumulator-based state,
which keeps per-recipe state isolated and makes the approach composable.
* AddDependency: move per-project state to Accumulator and run regen in visitor
Replaces the PythonDependencyExecutionContextView-based state with per-project
ProjectState entries on the Accumulator. The scanner captures lock-file content
and the recipe-specific match predicate; the visitor applies the trait edit on
the live tree, refreshes the marker, runs lock regeneration, and caches the
result. The lock-file visit reads the cache and re-emits the regenerated
content. This composes correctly across CompositeRecipe chains because each
recipe's edit lands before the next recipe's edit phase begins.
* AddDependency: ctx side-channel for cross-recipe deps tree sync
Composite chains of dependency recipes need the lock-file branch to see
the latest deps tree (with prior recipe edits applied), not the original
captured by the scanner. The scanner runs on originals before any edit
phase, so a B that follows A would otherwise regenerate from A's pre-edit
input.
Move per-source compute into the visitor's pre-visit and use an
ExecutionContext side channel keyed by deps-file path:
- deps-file branch: trait edit on the live cursor, write the modified
tree to the ctx side channel, regenerate lock content if scanner
captured one for this project.
- lock-file branch: lazily compute on first visit by reading the live
deps tree from ctx (or the scanner-captured fallback when no prior
recipe touched this path), reapplying the trait edit through a
synthetic Cursor rooted at the visitor's root cursor.
This works across composite recipe chains because the side channel is
ExecutionContext-scoped, and tolerates within-cycle visit-order
variations because the lock branch lazy-computes whenever it visits
first.
* AddDependency / PyProjectHelper: post-prototype simplifications
Cross-confirmed findings from /simplify review:
- Drop unused ProjectState.lockFilePath (set but never read).
- Drop overridden generate() that returned an empty list — it's the
ScanningRecipe default.
- Drop the parallel RegenerationResult value type — collapse onto
LockFileRegeneration.Result, which has the same shape (success flag,
lock content, error message). PyProjectHelper.regenerateLockContent
now returns LockFileRegeneration.Result directly.
- Move the PackageManager → adapter switch onto LockFileRegeneration
itself (LockFileRegeneration.forPackageManager(pm)). PyProjectHelper
no longer encodes the UV/PIPENV mapping.
- Replace synthetic-cursor workaround new Cursor(getCursor().getRoot(),
depsTree) with the idiomatic two-level form used by
TraitMatcher.lower(SourceFile): new Cursor(new Cursor(null,
Cursor.ROOT_VALUE), depsTree). Decouples the lock-branch trait match
from the visitor's own cursor state.
- Reuse the visitor's matcher field on the lock branch instead of
allocating a fresh PythonDependencyFile.Matcher per visit.
- Hoist the depsPath null-check and lockPs lookup with early returns,
reducing nesting in the lock branch.
- Drop narrative comments that restate obvious code.
- Tighten imports in PyProjectHelper (use HashMap import, drop
java.util.function.Function FQN where the import already exists).
* AddDependency / PyProjectHelper: collapse regen state, mark old API deprecated
Post-review cleanup before mirroring to RemoveDependency / ChangeDependency
/ UpgradeDependencyVersion / UpgradeTransitiveDependencyVersion:
- Collapse ProjectState.regeneratedLockContent + regenerationError into a
single nullable LockFileRegeneration.Result. The two fields were
mutually exclusive by construction (editAndRegenerate populates one or
the other on a regen attempt) — keeping them parallel rebuilt the same
redundancy that the previous /simplify pass already removed from
EditAndRegenerateResult. Same change applied to EditAndRegenerateResult
itself: the changed(modified, regen, error) factory is now changed(
modified, @nullable Result). The visitor reads
ps.regenResult.isSuccess() / getLockFileContent() / getErrorMessage()
directly. The ensureComputed idempotence guard simplifies to a single
modifiedDepsFile null-check (the regenerationError half was dead since
modifiedDepsFile is also set in the error branch).
- Mark the legacy ExecutionContext-based PyProjectHelper helpers
@deprecated: captureExistingLockContent, maybeReplayLockContent,
maybeUpdateUvLock, maybeUpdatePipfileLock, regenerateLockAndRefreshMarker,
regeneratePipfileLockAndRefreshMarker. They will be deleted in Task 8 of
the Accumulator rework, but until then the @deprecated marker prevents
the four sibling-recipe rewrites from accidentally reaching for them.
- Sync the plan's ProjectState shape with the implementation.
* Plan: sync Phase responsibilities with simplified ProjectState shape
* RemoveDependency: ctx side-channel for cross-recipe deps tree sync
* ChangeDependency: ctx side-channel for cross-recipe deps tree sync
* Plan: fix Task 4 template (ChangeDependency has no scope/groupName)
* UpgradeDependencyVersion: ctx side-channel for cross-recipe deps tree sync
* UpgradeTransitiveDependencyVersion: ctx side-channel for cross-recipe deps tree sync
* Remove afterModification from PythonDependencyFile trait
* Drop PythonDependencyExecutionContextView and unused PyProjectHelper helpers
* Remove internal planning doc
* Note in dependency-recipe descriptions that they aren't safe as preconditions
* Refresh PythonResolutionResult resolved deps after recipe edits
Recipes that edit a deps file and successfully regenerate its lock file
now overlay the resolved-dependency information from the regenerated
lock content onto the source file's PythonResolutionResult marker.
Previously editAndRegenerate only refreshed the declared-dep half of
the marker, so any subsequent recipe (or downstream consumer) reading
resolved dependencies still saw the pre-edit lock state.
Implementation:
- Extract the applyResolution + linkResolved + linkResolvedMap helpers
shared by PyProjectTomlParser and PipfileParser into a new
PythonResolutionLinker utility with two entry points
(applyPyproject / applyPipfile) covering the differing sets of
marker fields each format exposes.
- PyProjectHelper.applyResolvedDependencies(SourceFile, String)
dispatches on the marker's PackageManager (Uv -> UvLockParser,
Pipenv -> PipfileLockParser) to parse the lock content and run
it through the linker.
- editAndRegenerate calls this overlay step after a successful regen,
so the modifiedDepsFile returned to recipes carries a fully up-to-
date marker.
- AddDependencyTest gains a marker-overlay assertion: after adding
flask to a uv project, the post-recipe pyproject's marker contains
flask in resolvedDependencies and the declared flask Dependency is
linked to its ResolvedDependency entry.
No recipe-level changes; the helper boundary absorbs the new behavior.
* TomlParserVisitor: strip quotes from quoted-key identifier name
`Toml.Identifier.name` previously held the verbatim source for both bare
and quoted simple keys, so `"foo"` and `foo` produced different `name`
values even though the TOML spec treats them as the same key. Consumers
that matched on `getName()` had to strip quotes themselves at every site.
The class already has a separate `source` field for round-trip fidelity;
populate `name` with the unquoted form for simple keys while keeping
`source` as-is so the printer still emits the original.
Dotted keys are out of scope for this change. Without a dedicated
multi-segment AST type the parser cannot tell `site."google.com"` (two
segments, the second containing a literal dot) apart from `site.google.com`
(three segments) once both are flattened into a single `name` string, so
quoted segments inside a dotted key remain unstripped for now.
* Python deps recipes: handle quoted TOML keys and fix within-cycle chaining
Two bugs surfaced when running the dependency recipes via the Moderne
CLI against real Pipenv repositories.
**Quoted keys in `[packages]` / `[tool.pdm.overrides]`**
`Pipfile` and `pyproject.toml` allow quoted keys (e.g.
`"flake8" = "*"`). The recipes' match logic compared the package name
to `Toml.Identifier.getName()`, which now (with the rewrite-toml fix)
returns the unquoted form, so the comparisons just work. Removed the
ad-hoc instanceof+cast pattern from the call sites in favour of a
single null-safe `PyProjectHelper.extractKeyName(KeyValue)` helper used
by `PipfileFile`, `PipfileParser`, `PythonDependencyParser`, and
`PyProjectFile.upgradePdmOverride`. The PDM-overrides path also
silently ignored quoted keys via the same defect.
**Within-cycle chaining of recipes in a composite `recipeList`**
OpenRewrite's `RecipeRunCycle` runs every sub-recipe's scanner against
the original tree per cycle; only the edit phase chains within a
cycle. Each recipe was caching the scan-time `depsFileMatches` decision
and short-circuiting the visitor on it, so a downstream recipe in a
composite chain could not see additions/edits made by an earlier
recipe in the same cycle — convergence stretched across multiple
cycles, and lock-file regeneration ran on stale assumptions.
Drop the `depsFileMatches` field and the
`acc.projects.values().stream().noneMatch(...)` short-circuit in all 5
dependency recipes. Match decisions now happen at visit time against
the live trait obtained from the cursor (or via a synthetic cursor for
the lock-file lookahead path), so a chain like
`AddDependency → UpgradeDependencyVersion` for the same package
converges in a single cycle.
Tests:
- TOML-quoted-key coverage on all 5 recipe test classes (Pipfile and
PDM-overrides cases).
- New `ChangeDependencyTest.chainAddThenUpgradeAcrossRecipes` exercises
the within-cycle chain via a YAML composite recipe and asserts
single-cycle convergence.
---------
Co-authored-by: Knut Wannheden <knut@moderne.io>1 parent c4774bb commit ef603c1
36 files changed
Lines changed: 2132 additions & 766 deletions
File tree
- rewrite-json/src/main/java/org/openrewrite/json
- rewrite-python
- src
- integTest/java/org/openrewrite/python
- main/java/org/openrewrite/python
- internal
- rpc
- trait
- test/java/org/openrewrite/python
- internal
- rewrite-toml/src
- main/java/org/openrewrite/toml
- internal
- test/java/org/openrewrite/toml
Lines changed: 2 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
78 | 78 | | |
79 | 79 | | |
80 | 80 | | |
81 | | - | |
| 81 | + | |
| 82 | + | |
82 | 83 | | |
83 | 84 | | |
84 | 85 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
24 | 24 | | |
25 | 25 | | |
26 | 26 | | |
| 27 | + | |
27 | 28 | | |
28 | 29 | | |
29 | 30 | | |
| |||
126 | 127 | | |
127 | 128 | | |
128 | 129 | | |
| 130 | + | |
129 | 131 | | |
130 | 132 | | |
131 | 133 | | |
| |||
Lines changed: 42 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
22 | 22 | | |
23 | 23 | | |
24 | 24 | | |
| 25 | + | |
25 | 26 | | |
26 | 27 | | |
27 | 28 | | |
| |||
225 | 226 | | |
226 | 227 | | |
227 | 228 | | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
228 | 270 | | |
229 | 271 | | |
230 | 272 | | |
| |||
Lines changed: 102 additions & 27 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
19 | 19 | | |
20 | 20 | | |
21 | 21 | | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
22 | 25 | | |
23 | | - | |
24 | 26 | | |
25 | 27 | | |
26 | 28 | | |
27 | 29 | | |
28 | 30 | | |
29 | | - | |
| 31 | + | |
30 | 32 | | |
31 | | - | |
| 33 | + | |
32 | 34 | | |
33 | 35 | | |
34 | 36 | | |
35 | 37 | | |
36 | | - | |
| 38 | + | |
| 39 | + | |
37 | 40 | | |
38 | 41 | | |
39 | 42 | | |
| |||
90 | 93 | | |
91 | 94 | | |
92 | 95 | | |
93 | | - | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
94 | 100 | | |
95 | 101 | | |
96 | 102 | | |
97 | | - | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
98 | 112 | | |
99 | 113 | | |
100 | 114 | | |
| |||
114 | 128 | | |
115 | 129 | | |
116 | 130 | | |
117 | | - | |
118 | | - | |
119 | | - | |
120 | | - | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
121 | 145 | | |
122 | 146 | | |
| 147 | + | |
123 | 148 | | |
124 | | - | |
125 | | - | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
126 | 152 | | |
127 | 153 | | |
128 | 154 | | |
129 | 155 | | |
130 | 156 | | |
131 | 157 | | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
132 | 163 | | |
133 | 164 | | |
134 | | - | |
135 | | - | |
136 | | - | |
137 | 165 | | |
138 | 166 | | |
139 | 167 | | |
| |||
146 | 174 | | |
147 | 175 | | |
148 | 176 | | |
149 | | - | |
| 177 | + | |
| 178 | + | |
150 | 179 | | |
151 | | - | |
152 | | - | |
153 | | - | |
154 | | - | |
155 | | - | |
156 | | - | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
157 | 188 | | |
| 189 | + | |
| 190 | + | |
158 | 191 | | |
159 | 192 | | |
160 | 193 | | |
161 | | - | |
162 | | - | |
163 | | - | |
164 | | - | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
165 | 225 | | |
166 | 226 | | |
167 | | - | |
168 | 227 | | |
169 | 228 | | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
170 | 245 | | |
171 | 246 | | |
172 | 247 | | |
| |||
0 commit comments