11import type { Pep440Version } from '@renovatebot/pep440' ;
2- import { compare , explain as parse , rcompare } from '@renovatebot/pep440' ;
2+ import { compare , explain as parse } from '@renovatebot/pep440' ;
33import {
44 CancellationError ,
55 Disposable ,
@@ -21,10 +21,26 @@ import {
2121 PythonEnvironment ,
2222 PythonEnvironmentApi ,
2323} from '../../api' ;
24+ import { withProgress } from '../../common/window.apis' ;
25+ import { CommandConstructorOptions } from '../base/commands/index' ;
2426import { updatePackagesAndNotify } from '../common/packageChanges' ;
25- import { runPython , runUV , shouldUseUv } from './helpers' ;
27+ import { createPipOrUvCommand } from './commands/factory' ;
28+ import {
29+ PipAvailableVersionsCommand ,
30+ PipInstallCommand ,
31+ PipListCommand ,
32+ PipListDirectNamesCommand ,
33+ PipUninstallCommand ,
34+ PipVersionCommand ,
35+ UvAvailableVersionsCommand ,
36+ UvInstallCommand ,
37+ UvListCommand ,
38+ UvListDirectNamesCommand ,
39+ UvUninstallCommand ,
40+ UvVersionCommand ,
41+ } from './commands/index' ;
2642import { getWorkspacePackagesToInstall } from './pipUtils' ;
27- import { managePackages , normalizePackageName , refreshPipDirectPackageNames , refreshPipPackages } from './utils' ;
43+ import { parsePackageSpecs } from './utils' ;
2844import { VenvManager } from './venvManager' ;
2945
3046export class PipPackageManager implements PackageManager , Disposable {
@@ -65,43 +81,66 @@ export class PipPackageManager implements PackageManager, Disposable {
6581 }
6682 }
6783
68- const manageOptions = {
69- ...options ,
70- install : toInstall ,
71- uninstall : toUninstall ,
72- } ;
73- await window . withProgress (
74- {
75- location : ProgressLocation . Notification ,
76- title : 'Installing packages' ,
77- cancellable : true ,
78- } ,
79- async ( _progress , token ) => {
80- try {
81- await managePackages ( environment , manageOptions , this , token ) ;
82- await updatePackagesAndNotify (
83- this ,
84- environment ,
85- this . packages . get ( environment . envId . id ) ,
86- ( changes ) => {
87- this . _onDidChangePackages . fire ( { environment, manager : this , changes } ) ;
88- } ,
89- ) ;
90- } catch ( e ) {
91- if ( e instanceof CancellationError ) {
92- throw e ;
93- }
94- this . log . error ( 'Error managing packages' , e ) ;
95- setImmediate ( async ( ) => {
96- const result = await window . showErrorMessage ( 'Error managing packages' , 'View Output' ) ;
97- if ( result === 'View Output' ) {
98- this . log . show ( ) ;
99- }
100- } ) ;
101- throw e ;
84+ try {
85+ const pythonExecutable = environment . execInfo ?. run ?. executable ;
86+ if ( ! pythonExecutable ) {
87+ throw new Error ( 'Unable to determine Python executable path' ) ;
88+ }
89+
90+ // Centralize command options for install/uninstall operations
91+ const manageCommandOptions : CommandConstructorOptions = {
92+ pythonExecutable,
93+ log : this . log ,
94+ } ;
95+
96+ // Execute uninstall if needed
97+ if ( toUninstall . length > 0 ) {
98+ const uninstallCmd : PipUninstallCommand | UvUninstallCommand = await createPipOrUvCommand (
99+ manageCommandOptions ,
100+ environment . environmentPath . fsPath ,
101+ PipUninstallCommand ,
102+ UvUninstallCommand ,
103+ ) ;
104+ const packages = parsePackageSpecs ( toUninstall ) ;
105+ await withProgress (
106+ { location : ProgressLocation . Notification , title : 'Installing packages' , cancellable : true } ,
107+ ( _progress , token ) => uninstallCmd . execute ( { packages, cancellationToken : token } ) ,
108+ ) ;
109+ }
110+
111+ // Execute install if needed
112+ if ( toInstall . length > 0 ) {
113+ const installCmd : PipInstallCommand | UvInstallCommand = await createPipOrUvCommand (
114+ manageCommandOptions ,
115+ environment . environmentPath . fsPath ,
116+ PipInstallCommand ,
117+ UvInstallCommand ,
118+ ) ;
119+ const packages = parsePackageSpecs ( toInstall ) ;
120+ await withProgress (
121+ { location : ProgressLocation . Notification , title : 'Installing packages' , cancellable : true } ,
122+ ( _progress , token ) =>
123+ installCmd . execute ( { packages, upgrade : options . upgrade , cancellationToken : token } ) ,
124+ ) ;
125+ }
126+
127+ await updatePackagesAndNotify ( this , environment , this . packages . get ( environment . envId . id ) , ( changes ) => {
128+ this . _onDidChangePackages . fire ( { environment, manager : this , changes } ) ;
129+ } ) ;
130+ } catch ( e ) {
131+ if ( e instanceof CancellationError ) {
132+ // Cancellation is a normal control-flow exit; do not surface an error.
133+ return ;
134+ }
135+ this . log . error ( 'Error managing packages' , e ) ;
136+ setImmediate ( async ( ) => {
137+ const result = await window . showErrorMessage ( 'Error managing packages' , 'View Output' ) ;
138+ if ( result === 'View Output' ) {
139+ this . log . show ( ) ;
102140 }
103- } ,
104- ) ;
141+ } ) ;
142+ throw e ;
143+ }
105144 }
106145
107146 async refresh ( environment : PythonEnvironment ) : Promise < Package [ ] | undefined > {
@@ -125,7 +164,20 @@ export class PipPackageManager implements PackageManager, Disposable {
125164
126165 async getPackages ( environment : PythonEnvironment , options ?: GetPackagesOptions ) : Promise < Package [ ] | undefined > {
127166 if ( options ?. skipCache || ! this . packages . has ( environment . envId . id ) ) {
128- const data = await refreshPipPackages ( environment , this . log ) ;
167+ const pythonExecutable = environment . execInfo ?. run ?. executable ;
168+ if ( ! pythonExecutable ) {
169+ return undefined ;
170+ }
171+ const listCmd : PipListCommand | UvListCommand = await createPipOrUvCommand (
172+ {
173+ pythonExecutable,
174+ log : this . log ,
175+ } ,
176+ environment . environmentPath . fsPath ,
177+ PipListCommand ,
178+ UvListCommand ,
179+ ) ;
180+ const data = await listCmd . execute ( ) ;
129181 const packages = ( data ?? [ ] ) . map ( ( pkg ) => this . api . createPackageItem ( pkg , environment , this ) ) ;
130182 this . packages . set ( environment . envId . id , packages ) ;
131183 return packages ;
@@ -135,22 +187,17 @@ export class PipPackageManager implements PackageManager, Disposable {
135187
136188 async getVersion ( environment : PythonEnvironment ) : Promise < Pep440Version | undefined > {
137189 try {
138- const useUv = await shouldUseUv ( this . log , environment . environmentPath . fsPath ) ;
139- if ( useUv ) {
140- const result = await runUV ( [ '--version' ] , undefined , this . log ) ;
141- // "uv X.Y.Z"
142- const match = result . match ( / ^ u v \s + ( \d + \. \d + (?: \. \d + ) * ) / ) ;
143- return match ? ( parse ( match [ 1 ] ) ?? undefined ) : undefined ;
190+ const pythonExecutable = environment . execInfo ?. run ?. executable ;
191+ if ( ! pythonExecutable ) {
192+ return undefined ;
144193 }
145- const result = await runPython (
146- environment . execInfo ?. run ?. executable ?? 'python' ,
147- [ '-m' , 'pip' , '--version' ] ,
148- undefined ,
149- this . log ,
194+ const versionCmd : PipVersionCommand | UvVersionCommand = await createPipOrUvCommand (
195+ { pythonExecutable , log : this . log } ,
196+ environment . environmentPath . fsPath ,
197+ PipVersionCommand ,
198+ UvVersionCommand ,
150199 ) ;
151- // "pip X.Y.Z from /path/to/pip (python X.Y)"
152- const match = result . match ( / ^ p i p \s + ( \d + \. \d + (?: \. \d + ) * ) / ) ;
153- return match ? ( parse ( match [ 1 ] ) ?? undefined ) : undefined ;
200+ return await versionCmd . execute ( ) ;
154201 } catch {
155202 return undefined ;
156203 }
@@ -161,39 +208,45 @@ export class PipPackageManager implements PackageManager, Disposable {
161208 packageName : string ,
162209 ) : Promise < Pep440Version [ ] | undefined > {
163210 try {
164- const python = environment . execInfo ?. run ?. executable ;
165- if ( ! python ) {
211+ const pythonExecutable = environment . execInfo ?. run ?. executable ;
212+ if ( ! pythonExecutable ) {
166213 return undefined ;
167214 }
168215
169- const baseVersion = parse ( environment . version ) ?. base_version ;
216+ // Normalize versions like '3.13.1.final.0' (Python's sys.version_info format) to '3.13.1'
217+ // before parsing, since pep440 only accepts valid PEP 440 version strings.
218+ const versionMatch = ( environment . version ?? '' ) . match ( / ^ ( \d + (?: \. \d + ) * ) / ) ;
219+ const normalizedVersion = versionMatch ?. [ 1 ] ?? '' ;
220+ const baseVersion = parse ( normalizedVersion ) ?. base_version ;
170221 if ( ! baseVersion ) {
171222 return undefined ;
172223 }
173- // uv - Run pip via `uv tool run pip`
174- const useUv = await shouldUseUv ( this . log , environment . environmentPath . fsPath ) ;
175- if ( useUv ) {
176- const output = await runUV (
177- [ 'tool' , 'run' , 'pip' , 'index' , 'versions' , packageName , '--json' , '--python-version' , baseVersion ] ,
178- undefined ,
179- this . log ,
180- ) ;
181- return parsePipIndexVersionsJson ( output ) ;
182- }
183224
184- // pip >= 21.2.0 - use `pip index versions <package> --json` to get available versions in a machine readable format.
185- const pipVersion = await this . getVersion ( environment ) ;
186- if ( pipVersion && compare ( pipVersion . public , '21.2.0' ) >= 0 ) {
187- const output = await runPython (
188- python ,
189- [ '-m' , 'pip' , 'index' , 'versions' , packageName , '--json' , '--python-version' , baseVersion ] ,
190- undefined ,
191- this . log ,
225+ const availableVersionsCmd : PipAvailableVersionsCommand | UvAvailableVersionsCommand =
226+ await createPipOrUvCommand (
227+ { pythonExecutable, log : this . log } ,
228+ environment . environmentPath . fsPath ,
229+ PipAvailableVersionsCommand ,
230+ UvAvailableVersionsCommand ,
192231 ) ;
193- return parsePipIndexVersionsJson ( output ) ;
232+
233+ // For pip < 21.2.0, check version first
234+ if ( availableVersionsCmd instanceof PipAvailableVersionsCommand ) {
235+ const pipVersion = await this . getVersion ( environment ) ;
236+ if ( ! pipVersion || compare ( pipVersion . public , '21.2.0' ) < 0 ) {
237+ // pip <= 20.3.4 - version picking is undefined; no reliable machine-readable API exists.
238+ return undefined ;
239+ }
194240 }
195241
196- // pip <= 20.3.4 - version picking is undefined; no reliable machine-readable API exists.
242+ const versionStrings = await availableVersionsCmd . execute ( {
243+ packageName,
244+ pythonVersion : baseVersion ,
245+ } ) ;
246+ return versionStrings
247+ . map ( ( v ) => parse ( v ) )
248+ . filter ( ( parsed ) : parsed is Pep440Version => parsed !== null )
249+ . sort ( ( a , b ) => compare ( b . public , a . public ) ) ;
197250 } catch {
198251 return undefined ;
199252 }
@@ -205,38 +258,22 @@ export class PipPackageManager implements PackageManager, Disposable {
205258 }
206259
207260 /**
208- * Returns direct (non-transitive) package names using `pip list --not-required` or `uv pip tree --depth=0 `.
261+ * Returns direct (non-transitive) package names using `pip list --not-required` or `uv pip list --not-required `.
209262 *
210263 * Note: These commands return packages with no installed dependents (leaf packages), not packages
211264 * the user explicitly installed. pip/uv do not track install intent.
212265 */
213266 async getDirectPackageNames ( environment : PythonEnvironment ) : Promise < Set < string > | undefined > {
214- const data = await refreshPipDirectPackageNames ( environment , this . log ) ;
215- return data ? new Set ( data . map ( normalizePackageName ) ) : undefined ;
216- }
217- }
218-
219- /**
220- * Parses JSON output from `pip index versions <package> --json`.
221- * Expected format: { "name": "...", "versions": ["1.2.3", "1.2.2", ...] }
222- */
223- export function parsePipIndexVersionsJson ( output : string ) : Pep440Version [ ] | undefined {
224- // Only capture output between braces
225- const match = output . match ( / { [ \s \S ] * } / ) ;
226- if ( ! match ) {
227- return undefined ;
228- }
229- try {
230- const parsed = JSON . parse ( match [ 0 ] ) ;
231- if ( parsed && Array . isArray ( parsed . versions ) && parsed . versions . length > 0 ) {
232- return ( parsed . versions as string [ ] )
233- . filter ( ( v ) => ! ! v . trim ( ) )
234- . map ( ( v ) => parse ( v . trim ( ) ) )
235- . filter ( ( v ) : v is Pep440Version => v !== null )
236- . sort ( ( a , b ) => rcompare ( a . public , b . public ) ) ;
267+ const pythonExecutable = environment . execInfo ?. run ?. executable ;
268+ if ( ! pythonExecutable ) {
269+ return undefined ;
237270 }
238- return undefined ;
239- } catch {
240- return undefined ;
271+ const listDirectNamesCmd : PipListDirectNamesCommand | UvListDirectNamesCommand = await createPipOrUvCommand (
272+ { pythonExecutable, log : this . log } ,
273+ environment . environmentPath . fsPath ,
274+ PipListDirectNamesCommand ,
275+ UvListDirectNamesCommand ,
276+ ) ;
277+ return listDirectNamesCmd . execute ( ) ;
241278 }
242279}
0 commit comments