Skip to content

Commit 4f5be9f

Browse files
authored
Add --user flag to global dvc auto installation (#4091)
1 parent 00d14e2 commit 4f5be9f

File tree

7 files changed

+243
-46
lines changed

7 files changed

+243
-46
lines changed

extension/src/extensions/python.test.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import { extensions } from 'vscode'
22
import {
33
getPythonBinPath,
44
getOnDidChangePythonExecutionDetails,
5-
VscodePython
5+
VscodePython,
6+
isActivePythonEnvGlobal
67
} from './python'
78
import { executeProcess } from '../process/execution'
89

@@ -17,6 +18,7 @@ mockedExtensions.getExtension = mockedGetExtension
1718

1819
const mockedReady = jest.fn()
1920
const mockedOnDidChangeExecutionDetails = jest.fn()
21+
const mockedGetActiveEnvironmentPath = jest.fn()
2022
let mockedExecCommand: string[] | undefined
2123

2224
const mockedSettings = {
@@ -26,7 +28,16 @@ const mockedSettings = {
2628
onDidChangeExecutionDetails: mockedOnDidChangeExecutionDetails
2729
}
2830

31+
const mockedEnvironments = {
32+
getActiveEnvironmentPath: mockedGetActiveEnvironmentPath,
33+
known: [
34+
{ id: '/usr/bin/python' },
35+
{ environment: { type: 'VirtualEnvironment' }, id: '/.venv/bin/python' }
36+
]
37+
}
38+
2939
const mockedVscodePythonAPI = {
40+
environments: mockedEnvironments,
3041
ready: mockedReady,
3142
settings: mockedSettings
3243
} as unknown as VscodePython
@@ -63,6 +74,28 @@ describe('getPythonBinPath', () => {
6374
})
6475
})
6576

77+
describe('isActivePythonEnvGlobal', () => {
78+
it('should return true if active env is global', async () => {
79+
mockedGetActiveEnvironmentPath.mockReturnValueOnce({
80+
id: '/usr/bin/python'
81+
})
82+
83+
const result = await isActivePythonEnvGlobal()
84+
85+
expect(result).toStrictEqual(true)
86+
})
87+
88+
it('should return false if active env is not global', async () => {
89+
mockedGetActiveEnvironmentPath.mockReturnValueOnce({
90+
id: '/.venv/bin/python'
91+
})
92+
93+
const result = await isActivePythonEnvGlobal()
94+
95+
expect(result).toStrictEqual(false)
96+
})
97+
})
98+
6699
describe('getOnDidChangePythonExecutionDetails', () => {
67100
it('should return the listener if the python ready promise rejects', async () => {
68101
mockedReady.mockRejectedValueOnce(undefined)

extension/src/extensions/python.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,19 @@ type EnvironmentVariablesChangeEvent = {
1616
readonly env: EnvironmentVariables
1717
}
1818

19+
interface Environment {
20+
id: string
21+
environment?: {
22+
type: string
23+
}
24+
}
25+
1926
export interface VscodePython {
2027
ready: Thenable<void>
2128
settings: Settings
2229
environments: {
30+
known: Environment[]
31+
getActiveEnvironmentPath: () => { id: string }
2332
onDidEnvironmentVariablesChange: Event<EnvironmentVariablesChangeEvent>
2433
getEnvironmentVariables(): EnvironmentVariables
2534
}
@@ -56,6 +65,18 @@ export const getPYTHONPATH = async (): Promise<string | undefined> => {
5665
return api?.environments?.getEnvironmentVariables().PYTHONPATH
5766
}
5867

68+
export const isActivePythonEnvGlobal = async (): Promise<
69+
boolean | undefined
70+
> => {
71+
const api = await getPythonExtensionAPI()
72+
if (!api?.environments) {
73+
return
74+
}
75+
const envPath = api.environments.getActiveEnvironmentPath()
76+
const activeEnv = api.environments.known.find(({ id }) => id === envPath.id)
77+
return activeEnv && !activeEnv.environment
78+
}
79+
5980
export const getOnDidChangePythonExecutionDetails = async () => {
6081
const api = await getPythonExtensionAPI()
6182
return api?.settings?.onDidChangeExecutionDetails

extension/src/setup/autoInstall.ts

Lines changed: 83 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import { getPythonExecutionDetails } from '../extensions/python'
1+
import { Progress } from 'vscode'
2+
import {
3+
getPythonExecutionDetails,
4+
isActivePythonEnvGlobal
5+
} from '../extensions/python'
26
import { findPythonBin, getDefaultPython, installPackages } from '../python'
37
import { ConfigKey, getConfigValue } from '../vscode/config'
48
import { getFirstWorkspaceFolder } from '../vscode/workspaceFolders'
@@ -16,25 +20,59 @@ export const findPythonBinForInstall = async (): Promise<
1620
)
1721
}
1822

23+
const getProcessGlobalArgs = (isGlobal: boolean) => (isGlobal ? ['--user'] : [])
24+
25+
const installPackageAndIncrementProgress = ({
26+
root,
27+
pythonBinPath,
28+
isGlobalEnv,
29+
progress,
30+
incrementAmount,
31+
packageName,
32+
successMessage
33+
}: {
34+
root: string
35+
pythonBinPath: string
36+
isGlobalEnv: boolean
37+
progress: Progress<{ message: string; amount: number }>
38+
incrementAmount: number
39+
packageName: string
40+
successMessage: string
41+
}) =>
42+
Toast.runCommandAndIncrementProgress(
43+
async () => {
44+
await installPackages(
45+
root,
46+
pythonBinPath,
47+
...getProcessGlobalArgs(isGlobalEnv),
48+
packageName
49+
)
50+
return successMessage
51+
},
52+
progress,
53+
incrementAmount
54+
)
55+
1956
const showUpgradeProgress = (
2057
root: string,
21-
pythonBinPath: string
58+
pythonBinPath: string,
59+
isGlobalEnv: boolean
2260
): Thenable<unknown> =>
2361
Toast.showProgress('Upgrading DVC', async progress => {
2462
progress.report({ increment: 0 })
2563

2664
progress.report({ increment: 25, message: 'Updating packages...' })
2765

2866
try {
29-
await Toast.runCommandAndIncrementProgress(
30-
async () => {
31-
await installPackages(root, pythonBinPath, 'dvc')
32-
return 'Upgraded successfully'
33-
},
67+
await installPackageAndIncrementProgress({
68+
incrementAmount: 75,
69+
isGlobalEnv,
70+
packageName: 'dvc',
3471
progress,
35-
75
36-
)
37-
72+
pythonBinPath,
73+
root,
74+
successMessage: 'Upgraded successfully'
75+
})
3876
return Toast.delayProgressClosing()
3977
} catch (error: unknown) {
4078
return Toast.reportProgressError(error, progress)
@@ -43,33 +81,36 @@ const showUpgradeProgress = (
4381

4482
const showInstallProgress = (
4583
root: string,
46-
pythonBinPath: string
84+
pythonBinPath: string,
85+
isGlobalEnv: boolean
4786
): Thenable<unknown> =>
4887
Toast.showProgress('Installing packages', async progress => {
4988
progress.report({ increment: 0 })
5089

5190
try {
52-
await Toast.runCommandAndIncrementProgress(
53-
async () => {
54-
await installPackages(root, pythonBinPath, 'dvclive')
55-
return 'DVCLive Installed'
56-
},
91+
await installPackageAndIncrementProgress({
92+
incrementAmount: 25,
93+
isGlobalEnv,
94+
packageName: 'dvclive',
5795
progress,
58-
25
59-
)
96+
pythonBinPath,
97+
root,
98+
successMessage: 'DVCLive Installed'
99+
})
60100
} catch (error: unknown) {
61101
return Toast.reportProgressError(error, progress)
62102
}
63103

64104
try {
65-
await Toast.runCommandAndIncrementProgress(
66-
async () => {
67-
await installPackages(root, pythonBinPath, 'dvc')
68-
return 'DVC Installed'
69-
},
105+
await installPackageAndIncrementProgress({
106+
incrementAmount: 75,
107+
isGlobalEnv,
108+
packageName: 'dvc',
70109
progress,
71-
75
72-
)
110+
pythonBinPath,
111+
root,
112+
successMessage: 'DVC Installed'
113+
})
73114

74115
return Toast.delayProgressClosing()
75116
} catch (error: unknown) {
@@ -78,10 +119,17 @@ const showInstallProgress = (
78119
})
79120

80121
const getArgsAndRunCommand = async (
81-
command: (root: string, pythonBinPath: string) => Thenable<unknown>
122+
isPythonExtensionUsed: boolean,
123+
command: (
124+
root: string,
125+
pythonBinPath: string,
126+
isGlobalEnv: boolean
127+
) => Thenable<unknown>
82128
): Promise<unknown> => {
83129
const pythonBinPath = await findPythonBinForInstall()
84130
const root = getFirstWorkspaceFolder()
131+
const isPythonEnvGlobal =
132+
isPythonExtensionUsed && (await isActivePythonEnvGlobal())
85133

86134
if (!root) {
87135
return Toast.showError(
@@ -95,13 +143,17 @@ const getArgsAndRunCommand = async (
95143
)
96144
}
97145

98-
return command(root, pythonBinPath)
146+
return command(root, pythonBinPath, !!isPythonEnvGlobal)
99147
}
100148

101-
export const autoInstallDvc = (): Promise<unknown> => {
102-
return getArgsAndRunCommand(showInstallProgress)
149+
export const autoInstallDvc = (
150+
isPythonExtensionUsed: boolean
151+
): Promise<unknown> => {
152+
return getArgsAndRunCommand(isPythonExtensionUsed, showInstallProgress)
103153
}
104154

105-
export const autoUpgradeDvc = (): Promise<unknown> => {
106-
return getArgsAndRunCommand(showUpgradeProgress)
155+
export const autoUpgradeDvc = (
156+
isPythonExtensionUsed: boolean
157+
): Promise<unknown> => {
158+
return getArgsAndRunCommand(isPythonExtensionUsed, showUpgradeProgress)
107159
}

extension/src/setup/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,7 @@ export class Setup
420420
() => this.getWebview(),
421421
() => this.initializeGit(),
422422
(offline: boolean) => this.updateStudioOffline(offline),
423+
() => this.isPythonExtensionUsed(),
423424
() => this.updatePythonEnvironment()
424425
)
425426
this.dispose.track(

extension/src/setup/webview/messages.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,28 +8,31 @@ import {
88
import { BaseWebview } from '../../webview'
99
import { sendTelemetryEvent } from '../../telemetry'
1010
import { EventName } from '../../telemetry/constants'
11-
import { autoInstallDvc, autoUpgradeDvc } from '../autoInstall'
1211
import {
1312
RegisteredCliCommands,
1413
RegisteredCommands
1514
} from '../../commands/external'
1615
import { openUrl } from '../../vscode/external'
16+
import { autoInstallDvc, autoUpgradeDvc } from '../autoInstall'
1717

1818
export class WebviewMessages {
1919
private readonly getWebview: () => BaseWebview<TSetupData> | undefined
2020
private readonly initializeGit: () => void
2121
private readonly updateStudioOffline: (offline: boolean) => Promise<void>
22+
private readonly isPythonExtensionUsed: () => Promise<boolean>
2223
private readonly updatePythonEnv: () => Promise<void>
2324

2425
constructor(
2526
getWebview: () => BaseWebview<TSetupData> | undefined,
2627
initializeGit: () => void,
2728
updateStudioOffline: (shareLive: boolean) => Promise<void>,
29+
isPythonExtensionUsed: () => Promise<boolean>,
2830
updatePythonEnv: () => Promise<void>
2931
) {
3032
this.getWebview = getWebview
3133
this.initializeGit = initializeGit
3234
this.updateStudioOffline = updateStudioOffline
35+
this.isPythonExtensionUsed = isPythonExtensionUsed
3336
this.updatePythonEnv = updatePythonEnv
3437
}
3538

@@ -112,16 +115,20 @@ export class WebviewMessages {
112115
return this.updatePythonEnv()
113116
}
114117

115-
private upgradeDvc() {
118+
private async upgradeDvc() {
116119
sendTelemetryEvent(EventName.VIEWS_SETUP_UPGRADE_DVC, undefined, undefined)
117120

118-
return autoUpgradeDvc()
121+
const isPythonExtensionUsed = await this.isPythonExtensionUsed()
122+
123+
return autoUpgradeDvc(isPythonExtensionUsed)
119124
}
120125

121-
private installDvc() {
126+
private async installDvc() {
122127
sendTelemetryEvent(EventName.VIEWS_SETUP_INSTALL_DVC, undefined, undefined)
123128

124-
return autoInstallDvc()
129+
const isPythonExtensionUsed = await this.isPythonExtensionUsed()
130+
131+
return autoInstallDvc(isPythonExtensionUsed)
125132
}
126133

127134
private openStudio() {

0 commit comments

Comments
 (0)