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}
0 commit comments