Skip to content

Commit f43e7dd

Browse files
committed
Make UpgradeTransitiveDependencyVersion package-manager-aware
Add pdmOverrides field to PythonResolutionResult and detect package manager from pyproject.toml table names (tool.uv → Uv, tool.pdm → Pdm). UpgradeTransitiveDependencyVersion now branches by package manager: uv uses constraint-dependencies, PDM uses tool.pdm.overrides, and unknown managers fall back to adding a direct dependency.
1 parent ed00208 commit f43e7dd

8 files changed

Lines changed: 404 additions & 24 deletions

File tree

rewrite-python/src/main/java/org/openrewrite/python/PyProjectTomlParser.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ private PythonResolutionResult resolveFromLockFile(PythonResolutionResult marker
9292
marker = marker.withDependencyGroups(linkResolvedMap(marker.getDependencyGroups(), resolvedDeps));
9393
marker = marker.withConstraintDependencies(linkResolved(marker.getConstraintDependencies(), resolvedDeps));
9494
marker = marker.withOverrideDependencies(linkResolved(marker.getOverrideDependencies(), resolvedDeps));
95+
marker = marker.withPdmOverrides(linkResolved(marker.getPdmOverrides(), resolvedDeps));
9596

9697
return marker;
9798
}

rewrite-python/src/main/java/org/openrewrite/python/UpgradeTransitiveDependencyVersion.java

Lines changed: 130 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,12 @@
3535

3636
/**
3737
* Pin a transitive dependency version by adding or upgrading a constraint in the
38-
* appropriate tool-specific section. For uv projects, this uses
39-
* {@code [tool.uv].constraint-dependencies}.
38+
* appropriate tool-specific section. The strategy depends on the detected package manager:
39+
* <ul>
40+
* <li><b>uv</b>: uses {@code [tool.uv].constraint-dependencies}</li>
41+
* <li><b>PDM</b>: uses {@code [tool.pdm.overrides]}</li>
42+
* <li><b>Other/unknown</b>: adds as a direct dependency in {@code [project].dependencies}</li>
43+
* </ul>
4044
*/
4145
@EqualsAndHashCode(callSuper = false)
4246
@Value
@@ -64,15 +68,18 @@ public String getInstanceNameSuffix() {
6468

6569
@Override
6670
public String getDescription() {
67-
return "Pin a transitive dependency version by adding or upgrading a constraint. " +
68-
"For uv projects, this uses `[tool.uv].constraint-dependencies`. " +
69-
"When `uv` is available, the `uv.lock` file is regenerated.";
71+
return "Pin a transitive dependency version using the appropriate strategy for the " +
72+
"detected package manager: uv uses `[tool.uv].constraint-dependencies`, " +
73+
"PDM uses `[tool.pdm.overrides]`, and other managers add a direct dependency.";
7074
}
7175

7276
enum Action {
7377
NONE,
7478
ADD_CONSTRAINT,
75-
UPGRADE_CONSTRAINT
79+
UPGRADE_CONSTRAINT,
80+
ADD_PDM_OVERRIDE,
81+
UPGRADE_PDM_OVERRIDE,
82+
ADD_DIRECT_DEPENDENCY
7683
}
7784

7885
static class Accumulator {
@@ -108,23 +115,40 @@ public Toml.Document visitDocument(Toml.Document document, ExecutionContext ctx)
108115
return document;
109116
}
110117

111-
// For uv projects, require resolved dependencies to verify it's transitive
112-
if (marker.getResolvedDependency(packageName) == null) {
113-
return document;
114-
}
115-
116-
// Check if a constraint already exists
117-
Dependency existingConstraint = PyProjectHelper.findDependencyInScope(
118-
marker, packageName, "tool.uv.constraint-dependencies", null);
118+
PythonResolutionResult.PackageManager pm = marker.getPackageManager();
119+
Action action = null;
119120

120-
if (existingConstraint == null) {
121-
acc.actions.put(sourcePath, Action.ADD_CONSTRAINT);
122-
} else if (!version.equals(existingConstraint.getVersionConstraint())) {
123-
acc.actions.put(sourcePath, Action.UPGRADE_CONSTRAINT);
121+
if (pm == PythonResolutionResult.PackageManager.Uv) {
122+
// Uv: require resolved deps, use constraint-dependencies
123+
if (marker.getResolvedDependency(packageName) == null) {
124+
return document;
125+
}
126+
Dependency existing = PyProjectHelper.findDependencyInScope(
127+
marker, packageName, "tool.uv.constraint-dependencies", null);
128+
if (existing == null) {
129+
action = Action.ADD_CONSTRAINT;
130+
} else if (!version.equals(existing.getVersionConstraint())) {
131+
action = Action.UPGRADE_CONSTRAINT;
132+
}
133+
} else if (pm == PythonResolutionResult.PackageManager.Pdm) {
134+
// PDM: use tool.pdm.overrides
135+
Dependency existing = PyProjectHelper.findDependencyInScope(
136+
marker, packageName, "tool.pdm.overrides", null);
137+
if (existing == null) {
138+
action = Action.ADD_PDM_OVERRIDE;
139+
} else if (!version.equals(existing.getVersionConstraint())) {
140+
action = Action.UPGRADE_PDM_OVERRIDE;
141+
}
124142
} else {
143+
// Fallback: add as direct dependency
144+
action = Action.ADD_DIRECT_DEPENDENCY;
145+
}
146+
147+
if (action == null) {
125148
return document;
126149
}
127150

151+
acc.actions.put(sourcePath, action);
128152
acc.projectsToUpdate.add(sourcePath);
129153
return document;
130154
}
@@ -141,9 +165,15 @@ public Toml.Document visitDocument(Toml.Document document, ExecutionContext ctx)
141165
if (sourcePath.endsWith("pyproject.toml") && acc.projectsToUpdate.contains(sourcePath)) {
142166
Action action = acc.actions.get(sourcePath);
143167
if (action == Action.ADD_CONSTRAINT) {
144-
return addConstraint(document, ctx, acc);
168+
return addToArray(document, ctx, acc, "tool.uv.constraint-dependencies");
145169
} else if (action == Action.UPGRADE_CONSTRAINT) {
146170
return upgradeConstraint(document, ctx, acc);
171+
} else if (action == Action.ADD_PDM_OVERRIDE) {
172+
return addPdmOverride(document, ctx, acc);
173+
} else if (action == Action.UPGRADE_PDM_OVERRIDE) {
174+
return upgradePdmOverride(document, ctx, acc);
175+
} else if (action == Action.ADD_DIRECT_DEPENDENCY) {
176+
return addToArray(document, ctx, acc, null);
147177
}
148178
}
149179

@@ -160,15 +190,15 @@ public Toml.Document visitDocument(Toml.Document document, ExecutionContext ctx)
160190
};
161191
}
162192

163-
private Toml.Document addConstraint(Toml.Document document, ExecutionContext ctx, Accumulator acc) {
193+
private Toml.Document addToArray(Toml.Document document, ExecutionContext ctx, Accumulator acc, @Nullable String scope) {
164194
String pep508 = packageName + version;
165195

166196
Toml.Document updated = (Toml.Document) new TomlIsoVisitor<ExecutionContext>() {
167197
@Override
168198
public Toml.Array visitArray(Toml.Array array, ExecutionContext ctx) {
169199
Toml.Array a = super.visitArray(array, ctx);
170200

171-
if (!PyProjectHelper.isInsideDependencyArray(getCursor(), "tool.uv.constraint-dependencies", null)) {
201+
if (!PyProjectHelper.isInsideDependencyArray(getCursor(), scope, null)) {
172202
return a;
173203
}
174204

@@ -289,4 +319,83 @@ private boolean isInsideConstraintDependencies() {
289319

290320
return updated;
291321
}
322+
323+
private Toml.Document addPdmOverride(Toml.Document document, ExecutionContext ctx, Accumulator acc) {
324+
Toml.Document updated = (Toml.Document) new TomlIsoVisitor<ExecutionContext>() {
325+
@Override
326+
public Toml.Table visitTable(Toml.Table table, ExecutionContext ctx) {
327+
Toml.Table t = super.visitTable(table, ctx);
328+
if (t.getName() == null || !"tool.pdm.overrides".equals(t.getName().getName())) {
329+
return t;
330+
}
331+
332+
// Build a new KeyValue: packageName = "version"
333+
Toml.Identifier key = new Toml.Identifier(
334+
randomId(), Space.EMPTY, Markers.EMPTY, packageName, packageName);
335+
Toml.Literal value = new Toml.Literal(
336+
randomId(), Space.SINGLE_SPACE, Markers.EMPTY,
337+
TomlType.Primitive.String, "\"" + version + "\"", version);
338+
Toml.KeyValue newKv = new Toml.KeyValue(
339+
randomId(), Space.EMPTY, Markers.EMPTY,
340+
new TomlRightPadded<>(key, Space.SINGLE_SPACE, Markers.EMPTY),
341+
value);
342+
343+
// Determine prefix for new entry
344+
List<Toml> values = t.getValues();
345+
Space entryPrefix;
346+
if (!values.isEmpty()) {
347+
entryPrefix = values.get(values.size() - 1).getPrefix();
348+
} else {
349+
entryPrefix = Space.format("\n");
350+
}
351+
newKv = newKv.withPrefix(entryPrefix);
352+
353+
List<Toml> newValues = new ArrayList<>(values);
354+
newValues.add(newKv);
355+
return t.withValues(newValues);
356+
}
357+
}.visitNonNull(document, ctx);
358+
359+
if (updated != document) {
360+
updated = PyProjectHelper.regenerateLockAndRefreshMarker(updated, acc.updatedLockFiles);
361+
}
362+
363+
return updated;
364+
}
365+
366+
private Toml.Document upgradePdmOverride(Toml.Document document, ExecutionContext ctx, Accumulator acc) {
367+
String normalizedName = PythonResolutionResult.normalizeName(packageName);
368+
369+
Toml.Document updated = (Toml.Document) new TomlIsoVisitor<ExecutionContext>() {
370+
@Override
371+
public Toml.KeyValue visitKeyValue(Toml.KeyValue keyValue, ExecutionContext ctx) {
372+
Toml.KeyValue kv = super.visitKeyValue(keyValue, ctx);
373+
374+
if (!PyProjectHelper.isInsidePdmOverridesTable(getCursor())) {
375+
return kv;
376+
}
377+
378+
if (!(kv.getKey() instanceof Toml.Identifier)) {
379+
return kv;
380+
}
381+
String keyName = ((Toml.Identifier) kv.getKey()).getName();
382+
if (!PythonResolutionResult.normalizeName(keyName).equals(normalizedName)) {
383+
return kv;
384+
}
385+
386+
if (!(kv.getValue() instanceof Toml.Literal)) {
387+
return kv;
388+
}
389+
390+
Toml.Literal literal = (Toml.Literal) kv.getValue();
391+
return kv.withValue(literal.withSource("\"" + version + "\"").withValue(version));
392+
}
393+
}.visitNonNull(document, ctx);
394+
395+
if (updated != document) {
396+
updated = PyProjectHelper.regenerateLockAndRefreshMarker(updated, acc.updatedLockFiles);
397+
}
398+
399+
return updated;
400+
}
292401
}

rewrite-python/src/main/java/org/openrewrite/python/internal/PyProjectHelper.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,10 +253,31 @@ public static boolean isInsideDependencyArray(Cursor cursor, @Nullable String sc
253253
return findInList(marker.getConstraintDependencies(), packageName);
254254
} else if ("tool.uv.override-dependencies".equals(scope)) {
255255
return findInList(marker.getOverrideDependencies(), packageName);
256+
} else if ("tool.pdm.overrides".equals(scope)) {
257+
return findInList(marker.getPdmOverrides(), packageName);
256258
}
257259
return null;
258260
}
259261

262+
/**
263+
* Check whether a cursor path represents a position inside the
264+
* {@code [tool.pdm.overrides]} table in a pyproject.toml.
265+
*/
266+
public static boolean isInsidePdmOverridesTable(Cursor cursor) {
267+
Cursor c = cursor;
268+
while (c != null) {
269+
Object val = c.getValue();
270+
if (val instanceof Toml.Table) {
271+
Toml.Table table = (Toml.Table) val;
272+
if (table.getName() != null && "tool.pdm.overrides".equals(table.getName().getName())) {
273+
return true;
274+
}
275+
}
276+
c = c.getParent();
277+
}
278+
return false;
279+
}
280+
260281
private static @Nullable Dependency findInList(List<Dependency> deps, String packageName) {
261282
String normalized = PythonResolutionResult.normalizeName(packageName);
262283
for (Dependency dep : deps) {

rewrite-python/src/main/java/org/openrewrite/python/internal/PythonDependencyParser.java

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ public class PythonDependencyParser {
6666
List<Dependency> constraintDependencies = getDependencyList(uvTable, "constraint-dependencies");
6767
List<Dependency> overrideDependencies = getDependencyList(uvTable, "override-dependencies");
6868

69+
List<Dependency> pdmOverrides = getPdmOverrides(tables);
70+
71+
// Detect package manager from tool sections
72+
PythonResolutionResult.PackageManager packageManager = detectPackageManager(tables);
73+
6974
String path = doc.getSourcePath().toString();
7075

7176
return new PythonResolutionResult(
@@ -83,8 +88,9 @@ public class PythonDependencyParser {
8388
dependencyGroups,
8489
constraintDependencies,
8590
overrideDependencies,
91+
pdmOverrides,
8692
Collections.emptyList(),
87-
null,
93+
packageManager,
8894
null
8995
);
9096
}
@@ -273,6 +279,46 @@ private static Map<String, List<Dependency>> parseOptionalDependenciesFromTable(
273279
return result;
274280
}
275281

282+
/**
283+
* Extract PDM overrides from [tool.pdm.overrides].
284+
* Each entry is a key-value pair where key = package name, value = version constraint string.
285+
*/
286+
private static List<Dependency> getPdmOverrides(Map<String, Toml.Table> tables) {
287+
Toml.Table pdmOverridesTable = tables.get("tool.pdm.overrides");
288+
if (pdmOverridesTable == null) {
289+
return Collections.emptyList();
290+
}
291+
List<Dependency> result = new ArrayList<>();
292+
for (Toml value : pdmOverridesTable.getValues()) {
293+
if (value instanceof Toml.KeyValue) {
294+
Toml.KeyValue kv = (Toml.KeyValue) value;
295+
if (kv.getKey() instanceof Toml.Identifier && kv.getValue() instanceof Toml.Literal) {
296+
String pkgName = ((Toml.Identifier) kv.getKey()).getName();
297+
Object val = ((Toml.Literal) kv.getValue()).getValue();
298+
if (val instanceof String) {
299+
result.add(new Dependency(pkgName, (String) val, null, null, null));
300+
}
301+
}
302+
}
303+
}
304+
return result;
305+
}
306+
307+
/**
308+
* Detect the package manager from tool-specific table names.
309+
*/
310+
private static PythonResolutionResult.@Nullable PackageManager detectPackageManager(Map<String, Toml.Table> tables) {
311+
for (String tableName : tables.keySet()) {
312+
if ("tool.pdm".equals(tableName) || tableName.startsWith("tool.pdm.")) {
313+
return PythonResolutionResult.PackageManager.Pdm;
314+
}
315+
if ("tool.uv".equals(tableName) || tableName.startsWith("tool.uv.")) {
316+
return PythonResolutionResult.PackageManager.Uv;
317+
}
318+
}
319+
return null;
320+
}
321+
276322
// PEP 508 parsing: name[extras](version_constraint);marker
277323
// Examples:
278324
// requests

rewrite-python/src/main/java/org/openrewrite/python/marker/PythonResolutionResult.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,12 @@ public class PythonResolutionResult implements Marker, RpcCodec<PythonResolution
9999
*/
100100
List<Dependency> overrideDependencies;
101101

102+
/**
103+
* PDM overrides from [tool.pdm.overrides].
104+
* These force specific versions for packages in PDM-managed projects.
105+
*/
106+
List<Dependency> pdmOverrides;
107+
102108
List<ResolvedDependency> resolvedDependencies;
103109

104110
@Nullable PackageManager packageManager;
@@ -176,6 +182,11 @@ public class PythonResolutionResult implements Marker, RpcCodec<PythonResolution
176182
return dep;
177183
}
178184
}
185+
for (Dependency dep : pdmOverrides) {
186+
if (normalizeName(dep.getName()).equals(normalized)) {
187+
return dep;
188+
}
189+
}
179190
return null;
180191
}
181192

@@ -194,6 +205,7 @@ public List<Dependency> getAllDeclaredDependencies() {
194205
}
195206
all.addAll(constraintDependencies);
196207
all.addAll(overrideDependencies);
208+
all.addAll(pdmOverrides);
197209
return all;
198210
}
199211

@@ -228,6 +240,9 @@ public void rpcSend(PythonResolutionResult after, RpcSendQueue q) {
228240
q.getAndSendListAsRef(after, PythonResolutionResult::getOverrideDependencies,
229241
dep -> dep.getName() + "@" + dep.getVersionConstraint(),
230242
dep -> dep.rpcSend(dep, q));
243+
q.getAndSendListAsRef(after, PythonResolutionResult::getPdmOverrides,
244+
dep -> dep.getName() + "@" + dep.getVersionConstraint(),
245+
dep -> dep.rpcSend(dep, q));
231246
q.getAndSendListAsRef(after, PythonResolutionResult::getResolvedDependencies,
232247
resolved -> resolved.getName() + "@" + resolved.getVersion(),
233248
resolved -> resolved.rpcSend(resolved, q));
@@ -258,6 +273,8 @@ public PythonResolutionResult rpcReceive(PythonResolutionResult before, RpcRecei
258273
dep -> dep.rpcReceive(dep, q)))
259274
.withOverrideDependencies(q.receiveList(before.overrideDependencies,
260275
dep -> dep.rpcReceive(dep, q)))
276+
.withPdmOverrides(q.receiveList(before.pdmOverrides,
277+
dep -> dep.rpcReceive(dep, q)))
261278
.withResolvedDependencies(q.receiveList(before.resolvedDependencies,
262279
resolved -> resolved.rpcReceive(resolved, q)))
263280
.withPackageManager(q.receiveAndGet(before.packageManager, toEnum(PackageManager.class)))

0 commit comments

Comments
 (0)