Skip to content

Commit 16a79dc

Browse files
authored
Provide option to auto-install DVC if unavailable (#2944)
* update get started webview with new skeleton * add static options for screen * wire up select python interpreter * add rough implementation for installing dvc * reverse installing of DVCLive and DVC * move auto install * refactor * show loading screen while DVC tries to load experiments data * add further tests * self review * apply review suggestion
1 parent fcf61da commit 16a79dc

File tree

19 files changed

+615
-69
lines changed

19 files changed

+615
-69
lines changed

extension/src/experiments/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -497,7 +497,7 @@ export class Experiments extends BaseRepository<TableData> {
497497

498498
public getHasData() {
499499
if (this.deferred.state === 'none') {
500-
return false
500+
return
501501
}
502502
return this.columns.hasNonDefaultColumns()
503503
}

extension/src/experiments/workspace.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -329,9 +329,19 @@ export class WorkspaceExperiments extends BaseWorkspaceWebviews<
329329
}
330330

331331
public getHasData() {
332-
return Object.values(this.repositories).some(repository =>
333-
repository.getHasData()
334-
)
332+
const allLoading = undefined
333+
334+
const repositories = Object.values(this.repositories)
335+
336+
if (repositories.some(repository => repository.getHasData())) {
337+
return true
338+
}
339+
340+
if (repositories.some(repository => repository.getHasData() === false)) {
341+
return false
342+
}
343+
344+
return allLoading
335345
}
336346

337347
private async pickExpThenRun(

extension/src/extension.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@ export class Extension extends Disposable implements IExtension {
241241
this.config.onDidChangeExecutionDetails(async () => {
242242
const stopWatch = new StopWatch()
243243
try {
244+
this.changeSetupStep()
244245
await setup(this)
245246

246247
return sendTelemetryEvent(

extension/src/extensions/python.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { commands, Event, Uri } from 'vscode'
2-
import { executeProcess } from '../processExecution'
2+
import { findPythonBin } from '../python'
33
import { getExtensionAPI, isInstalled } from '../vscode/extensions'
44

55
const PYTHON_EXTENSION_ID = 'ms-python.python'
@@ -38,13 +38,7 @@ export const getPythonBinPath = async (): Promise<string | undefined> => {
3838
const pythonExecutionDetails = await getPythonExecutionDetails()
3939
const pythonBin = pythonExecutionDetails?.join(' ')
4040
if (pythonBin) {
41-
try {
42-
return await executeProcess({
43-
args: ['-c', 'import sys; print(sys.executable)'],
44-
cwd: process.cwd(),
45-
executable: pythonBin
46-
})
47-
} catch {}
41+
return findPythonBin(pythonBin)
4842
}
4943
}
5044

extension/src/fileSystem/index.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { basename, extname, join, relative, resolve } from 'path'
1+
import { basename, extname, join, relative, resolve, sep } from 'path'
22
import {
33
ensureFileSync,
44
existsSync,
@@ -16,6 +16,7 @@ import { Logger } from '../common/logger'
1616
import { gitPath } from '../cli/git/constants'
1717
import { createValidInteger } from '../util/number'
1818
import { processExists } from '../processExecution'
19+
import { getFirstWorkspaceFolder } from '../vscode/workspaceFolders'
1920

2021
export const exists = (path: string): boolean => existsSync(path)
2122

@@ -164,3 +165,20 @@ export const checkSignalFile = async (path: string): Promise<boolean> => {
164165

165166
return true
166167
}
168+
169+
export const getBinDisplayText = (
170+
path: string | undefined
171+
): string | undefined => {
172+
if (!path) {
173+
return
174+
}
175+
176+
const workspaceRoot = getFirstWorkspaceFolder()
177+
if (!workspaceRoot) {
178+
return path
179+
}
180+
181+
return isSameOrChild(workspaceRoot, path)
182+
? '.' + sep + relative(workspaceRoot, path)
183+
: path
184+
}

extension/src/python/index.ts

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,45 +3,67 @@ import { getVenvBinPath } from './path'
33
import { getProcessPlatform } from '../env'
44
import { exists } from '../fileSystem'
55
import { Logger } from '../common/logger'
6-
import { createProcess, Process, ProcessOptions } from '../processExecution'
6+
import { createProcess, executeProcess, Process } from '../processExecution'
77

8-
const sendOutput = (process: Process) =>
8+
const sendOutput = (process: Process) => {
99
process.all?.on('data', chunk =>
1010
Logger.log(chunk.toString().replace(/(\r?\n)/g, ''))
1111
)
12-
13-
export const createProcessWithOutput = (options: ProcessOptions) => {
14-
const process = createProcess(options)
15-
16-
sendOutput(process)
17-
1812
return process
1913
}
2014

21-
const installPackages = (cwd: string, pythonBin: string, ...args: string[]) => {
22-
return createProcessWithOutput({
15+
export const installPackages = (
16+
cwd: string,
17+
pythonBin: string,
18+
...args: string[]
19+
): Process => {
20+
const options = {
2321
args: ['-m', 'pip', 'install', '--upgrade', ...args],
2422
cwd,
2523
executable: pythonBin
26-
})
24+
}
25+
26+
return createProcess(options)
2727
}
2828

29+
export const getDefaultPython = (): string =>
30+
getProcessPlatform() === 'win32' ? 'python' : 'python3'
31+
2932
export const setupVenv = async (
3033
cwd: string,
3134
envDir: string,
3235
...installArgs: string[]
3336
) => {
3437
if (!exists(join(cwd, envDir))) {
35-
await createProcessWithOutput({
38+
const initVenv = createProcess({
3639
args: ['-m', 'venv', envDir],
3740
cwd,
38-
executable: getProcessPlatform() === 'win32' ? 'python' : 'python3'
41+
executable: getDefaultPython()
3942
})
43+
await sendOutput(initVenv)
4044
}
4145

4246
const venvPython = getVenvBinPath(cwd, envDir, 'python')
4347

44-
await installPackages(cwd, venvPython, 'pip', 'wheel')
48+
const venvUpgrade = installPackages(cwd, venvPython, 'pip', 'wheel')
49+
await sendOutput(venvUpgrade)
50+
51+
const installRequestedPackages = installPackages(
52+
cwd,
53+
venvPython,
54+
...installArgs
55+
)
56+
return sendOutput(installRequestedPackages)
57+
}
4558

46-
return installPackages(cwd, venvPython, ...installArgs)
59+
export const findPythonBin = async (
60+
pythonBin: string
61+
): Promise<string | undefined> => {
62+
try {
63+
return await executeProcess({
64+
args: ['-c', 'import sys; print(sys.executable)'],
65+
cwd: process.cwd(),
66+
executable: pythonBin
67+
})
68+
} catch {}
4769
}

extension/src/setup/autoInstall.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { getPythonExecutionDetails } from '../extensions/python'
2+
import { findPythonBin, getDefaultPython, installPackages } from '../python'
3+
import { ConfigKey, getConfigValue } from '../vscode/config'
4+
import { getFirstWorkspaceFolder } from '../vscode/workspaceFolders'
5+
import { Toast } from '../vscode/toast'
6+
7+
export const findPythonBinForInstall = async (): Promise<
8+
string | undefined
9+
> => {
10+
const manualPython = getConfigValue(ConfigKey.PYTHON_PATH)
11+
12+
const autoPythonDetails = await getPythonExecutionDetails()
13+
14+
return findPythonBin(
15+
manualPython || autoPythonDetails?.join('') || getDefaultPython()
16+
)
17+
}
18+
19+
const showInstallProgress = (
20+
root: string,
21+
pythonBinPath: string
22+
): Thenable<unknown> =>
23+
Toast.showProgress('Installing packages', async progress => {
24+
progress.report({ increment: 0 })
25+
26+
await Toast.runCommandAndIncrementProgress(
27+
async () => {
28+
await installPackages(root, pythonBinPath, 'dvclive')
29+
return 'DVCLive Installed'
30+
},
31+
progress,
32+
25
33+
)
34+
35+
await Toast.runCommandAndIncrementProgress(
36+
async () => {
37+
await installPackages(root, pythonBinPath, 'dvc')
38+
return 'DVC Installed'
39+
},
40+
progress,
41+
75
42+
)
43+
44+
return Toast.delayProgressClosing()
45+
})
46+
47+
export const autoInstallDvc = async (): Promise<unknown> => {
48+
const pythonBinPath = await findPythonBinForInstall()
49+
const root = getFirstWorkspaceFolder()
50+
51+
if (!root) {
52+
return Toast.showError(
53+
'DVC could not be auto-installed because there is no folder open in the workspace'
54+
)
55+
}
56+
57+
if (!pythonBinPath) {
58+
return Toast.showError(
59+
'DVC could not be auto-installed because a Python interpreter could not be located'
60+
)
61+
}
62+
63+
return showInstallProgress(root, pythonBinPath)
64+
}

extension/src/setup/index.ts

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,23 @@
11
import { SetupData as TSetupData } from './webview/contract'
22
import { WebviewMessages } from './webview/messages'
3+
import { findPythonBinForInstall } from './autoInstall'
34
import { BaseWebview } from '../webview'
45
import { ViewKey } from '../webview/constants'
56
import { BaseRepository } from '../webview/repository'
67
import { Resource } from '../resourceLocator'
8+
import { isPythonExtensionInstalled } from '../extensions/python'
9+
import { getBinDisplayText } from '../fileSystem'
710

811
export type SetupWebviewWebview = BaseWebview<TSetupData>
912

1013
export class Setup extends BaseRepository<TSetupData> {
1114
public readonly viewKey = ViewKey.SETUP
1215

1316
private webviewMessages: WebviewMessages
14-
private initProject: () => void
1517
private showExperiments: () => void
1618
private getCliAccessible: () => boolean
1719
private getHasRoots: () => boolean
18-
private getHasData: () => boolean
20+
private getHasData: () => boolean | undefined
1921

2022
constructor(
2123
dvcRoot: string,
@@ -24,27 +26,26 @@ export class Setup extends BaseRepository<TSetupData> {
2426
showExperiments: () => void,
2527
getCliAccessible: () => boolean,
2628
getHasRoots: () => boolean,
27-
getHasData: () => boolean
29+
getHasData: () => boolean | undefined
2830
) {
2931
super(dvcRoot, webviewIcon)
3032

31-
this.webviewMessages = this.createWebviewMessageHandler()
33+
this.webviewMessages = this.createWebviewMessageHandler(initProject)
3234

3335
if (this.webview) {
3436
this.sendDataToWebview()
3537
}
38+
this.showExperiments = showExperiments
3639
this.getCliAccessible = getCliAccessible
3740
this.getHasRoots = getHasRoots
38-
this.initProject = initProject
39-
this.showExperiments = showExperiments
4041
this.getHasData = getHasData
4142
}
4243

4344
public sendInitialWebviewData() {
4445
return this.sendDataToWebview()
4546
}
4647

47-
public sendDataToWebview() {
48+
public async sendDataToWebview() {
4849
const cliAccessible = this.getCliAccessible()
4950
const projectInitialized = this.getHasRoots()
5051
const hasData = this.getHasData()
@@ -60,17 +61,21 @@ export class Setup extends BaseRepository<TSetupData> {
6061
return
6162
}
6263

64+
const pythonBinPath = await findPythonBinForInstall()
65+
6366
this.webviewMessages.sendWebviewMessage(
6467
cliAccessible,
6568
projectInitialized,
69+
isPythonExtensionInstalled(),
70+
getBinDisplayText(pythonBinPath),
6671
hasData
6772
)
6873
}
6974

70-
private createWebviewMessageHandler() {
75+
private createWebviewMessageHandler(initProject: () => void) {
7176
const webviewMessages = new WebviewMessages(
7277
() => this.getWebview(),
73-
() => this.initProject()
78+
initProject
7479
)
7580
this.dispose.track(
7681
this.onDidReceivedWebviewMessage(message =>
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
export type SetupData = {
22
cliAccessible: boolean
3+
hasData: boolean | undefined
4+
isPythonExtensionInstalled: boolean
35
projectInitialized: boolean
4-
hasData: boolean
6+
pythonBinPath: string | undefined
57
}

0 commit comments

Comments
 (0)