Skip to content

Commit 4c8b08e

Browse files
committed
install pytest when configuring #459
1 parent 095e9ab commit 4c8b08e

5 files changed

Lines changed: 67 additions & 51 deletions

File tree

src/client/common/installer.ts

Lines changed: 38 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,17 @@ export enum Product {
1717
unittest
1818
}
1919

20-
const ProductInstallScripts = new Map<Product, string>();
21-
ProductInstallScripts.set(Product.autopep8, 'pip install autopep8');
22-
ProductInstallScripts.set(Product.flake8, 'pip install flake8');
23-
ProductInstallScripts.set(Product.mypy, 'pip install mypy-lang');
24-
ProductInstallScripts.set(Product.nosetest, 'pip install nose');
25-
ProductInstallScripts.set(Product.pep8, 'pip install pep8');
26-
ProductInstallScripts.set(Product.prospector, 'pip install prospector');
27-
ProductInstallScripts.set(Product.pydocstyle, 'pip install pydocstyle');
28-
ProductInstallScripts.set(Product.pylint, 'pip install pylint');
29-
ProductInstallScripts.set(Product.pytest, 'pip install -U pytest');
30-
ProductInstallScripts.set(Product.yapf, 'pip install yapf');
20+
const ProductInstallScripts = new Map<Product, string[]>();
21+
ProductInstallScripts.set(Product.autopep8, ['pip', 'install', 'autopep8']);
22+
ProductInstallScripts.set(Product.flake8, ['pip', 'install', 'flake8']);
23+
ProductInstallScripts.set(Product.mypy, ['pip', 'install', 'mypy-lang']);
24+
ProductInstallScripts.set(Product.nosetest, ['pip', 'install', 'nose']);
25+
ProductInstallScripts.set(Product.pep8, ['pip', 'install', 'pep8']);
26+
ProductInstallScripts.set(Product.prospector, ['pip', 'install', 'prospector']);
27+
ProductInstallScripts.set(Product.pydocstyle, ['pip', 'install', 'pydocstyle']);
28+
ProductInstallScripts.set(Product.pylint, ['pip', 'install', 'pylint']);
29+
ProductInstallScripts.set(Product.pytest, ['pip', 'install', '-U', 'pytest']);
30+
ProductInstallScripts.set(Product.yapf, ['pip', 'install', 'yapf']);
3131

3232

3333
const Linters: Product[] = [Product.flake8, Product.pep8, Product.prospector, Product.pylint, Product.mypy, Product.pydocstyle];
@@ -61,7 +61,7 @@ SettingToDisableProduct.set(Product.yapf, 'yapf');
6161
export class Installer {
6262
private static terminal: vscode.Terminal;
6363
private disposables: vscode.Disposable[] = [];
64-
constructor() {
64+
constructor(private outputChannel: vscode.OutputChannel = null) {
6565
this.disposables.push(vscode.window.onDidCloseTerminal(term => {
6666
if (term === Installer.terminal) {
6767
Installer.terminal = null;
@@ -72,7 +72,7 @@ export class Installer {
7272
this.disposables.forEach(d => d.dispose());
7373
}
7474

75-
promptToInstall(product: Product) {
75+
promptToInstall(product: Product): Thenable<any> {
7676
let productType = Linters.indexOf(product) >= 0 ? 'Linter' : (Formatters.indexOf(product) >= 0 ? 'Formatter' : 'Test Framework');
7777
const productName = ProductNames.get(product);
7878

@@ -87,53 +87,61 @@ export class Installer {
8787
else {
8888
options.push(...[installOption, useOtherFormatter]);
8989
}
90-
vscode.window.showErrorMessage(`${productType} ${productName} is not installed`, ...options).then(item => {
90+
return vscode.window.showErrorMessage(`${productType} ${productName} is not installed`, ...options).then(item => {
9191
switch (item) {
9292
case installOption: {
93-
this.installProduct(product);
94-
break;
93+
return this.installProduct(product);
9594
}
9695
case disableOption: {
9796
if (Linters.indexOf(product) >= 0) {
98-
disableLinter(product);
97+
return disableLinter(product);
9998
}
10099
else {
101100
const pythonConfig = vscode.workspace.getConfiguration('python');
102101
const settingToDisable = SettingToDisableProduct.get(product);
103-
pythonConfig.update(settingToDisable, false);
102+
return pythonConfig.update(settingToDisable, false);
104103
}
105-
break;
106104
}
107105
case useOtherFormatter: {
108106
const pythonConfig = vscode.workspace.getConfiguration('python');
109-
pythonConfig.update('formatting.provider', alternateFormatter);
110-
break;
107+
return pythonConfig.update('formatting.provider', alternateFormatter);
111108
}
112109
case 'Help': {
113-
break;
110+
return Promise.resolve();
114111
}
115112
}
116113
});
117114
}
118115

119-
installProduct(product: Product) {
120-
if (!Installer.terminal) {
116+
installProduct(product: Product): Promise<any> {
117+
if (!this.outputChannel && !Installer.terminal) {
121118
Installer.terminal = vscode.window.createTerminal('Python Installer');
122119
}
123120

124-
let installScript = ProductInstallScripts.get(product);
125-
if (installScript.startsWith('pip install')) {
126-
const pythonPath = settings.PythonSettings.getInstance().pythonPath;
121+
let installArgs = ProductInstallScripts.get(product);
122+
const pythonPath = settings.PythonSettings.getInstance().pythonPath;
123+
124+
if (this.outputChannel) {
125+
// Errors are just displayed to the user
126+
this.outputChannel.show();
127+
return execPythonFile(pythonPath, ['-m', ...installArgs], vscode.workspace.rootPath, true, (data) => {
128+
this.outputChannel.append(data);
129+
});
130+
}
131+
else {
132+
let installScript = installArgs.join(' ');
127133
if (pythonPath.indexOf(' ') >= 0) {
128134
installScript = `"${pythonPath}" -m ${installScript}`;
129135
}
130136
else {
131137
installScript = `${pythonPath} -m ${installScript}`;
132138
}
133-
}
134139

135-
Installer.terminal.sendText(installScript);
136-
Installer.terminal.show(false);
140+
Installer.terminal.sendText(installScript);
141+
Installer.terminal.show(false);
142+
// Unfortunately we won't know when the command has completed
143+
return Promise.resolve();
144+
}
137145
}
138146

139147
isProductInstalled(product: Product): Promise<boolean> {

src/client/unittests/common/testConfigurationManager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import * as path from 'path';
66
export abstract class TestConfigurationManager {
77
public abstract enable(): Thenable<any>;
88
public abstract disable(): Thenable<any>;
9-
9+
constructor(protected readonly outputChannel: vscode.OutputChannel) { }
1010
public abstract configure(rootDir: string): Promise<any>;
1111

1212
protected selectTestDir(rootDir: string, subDirs: string[], customOptions: vscode.QuickPickItem[] = []): Promise<string> {

src/client/unittests/configuration.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import * as path from 'path';
1111

1212
const settings = PythonSettings.getInstance();
1313

14-
function promptToEnableAndConfigureTestFramework(messageToDisplay: string = 'Select a test framework/tool to enable', enableOnly: boolean = false): Thenable<any> {
14+
function promptToEnableAndConfigureTestFramework(outputChannel: vscode.OutputChannel, messageToDisplay: string = 'Select a test framework/tool to enable', enableOnly: boolean = false): Thenable<any> {
1515
const items = [{
1616
label: 'unittest',
1717
product: Product.unittest,
@@ -42,15 +42,15 @@ function promptToEnableAndConfigureTestFramework(messageToDisplay: string = 'Sel
4242
let configMgr: TestConfigurationManager;
4343
switch (item.product) {
4444
case Product.unittest: {
45-
configMgr = new unittest.ConfigurationManager();
45+
configMgr = new unittest.ConfigurationManager(outputChannel);
4646
break;
4747
}
4848
case Product.pytest: {
49-
configMgr = new pytest.ConfigurationManager();
49+
configMgr = new pytest.ConfigurationManager(outputChannel);
5050
break;
5151
}
5252
case Product.nosetest: {
53-
configMgr = new nose.ConfigurationManager();
53+
configMgr = new nose.ConfigurationManager(outputChannel);
5454
break;
5555
}
5656
default: {
@@ -61,13 +61,13 @@ function promptToEnableAndConfigureTestFramework(messageToDisplay: string = 'Sel
6161
if (enableOnly) {
6262
// Ensure others are disabled
6363
if (item.product !== Product.unittest) {
64-
(new unittest.ConfigurationManager()).disable();
64+
(new unittest.ConfigurationManager(outputChannel)).disable();
6565
}
6666
if (item.product !== Product.pytest) {
67-
(new pytest.ConfigurationManager()).disable();
67+
(new pytest.ConfigurationManager(outputChannel)).disable();
6868
}
6969
if (item.product !== Product.nosetest) {
70-
(new nose.ConfigurationManager()).disable();
70+
(new nose.ConfigurationManager(outputChannel)).disable();
7171
}
7272
return configMgr.enable();
7373
}
@@ -93,24 +93,24 @@ function promptToEnableAndConfigureTestFramework(messageToDisplay: string = 'Sel
9393
});
9494
});
9595
}
96-
export function displayTestFrameworkError(): Thenable<any> {
96+
export function displayTestFrameworkError(outputChannel: vscode.OutputChannel): Thenable<any> {
9797
let enabledCount = settings.unitTest.pyTestEnabled ? 1 : 0;
9898
enabledCount += settings.unitTest.nosetestsEnabled ? 1 : 0;
9999
enabledCount += settings.unitTest.unittestEnabled ? 1 : 0;
100100
if (enabledCount > 1) {
101-
return promptToEnableAndConfigureTestFramework('Enable only one of the test frameworks (unittest, pytest or nosetest).', true);
101+
return promptToEnableAndConfigureTestFramework(outputChannel, 'Enable only one of the test frameworks (unittest, pytest or nosetest).', true);
102102
}
103103
else {
104104
const option = 'Enable and configure a Test Framework';
105105
return vscode.window.showInformationMessage('No test framework configured (unittest, pytest or nosetest)', option).then(item => {
106106
if (item === option) {
107-
return promptToEnableAndConfigureTestFramework();
107+
return promptToEnableAndConfigureTestFramework(outputChannel);
108108
}
109109
return Promise.reject(null);
110110
});
111111
}
112112
}
113-
export function displayPromptToEnableTests(rootDir: string): Thenable<any> {
113+
export function displayPromptToEnableTests(rootDir: string, outputChannel: vscode.OutputChannel): Thenable<any> {
114114
if (settings.unitTest.pyTestEnabled ||
115115
settings.unitTest.nosetestsEnabled ||
116116
settings.unitTest.unittestEnabled) {
@@ -134,7 +134,7 @@ export function displayPromptToEnableTests(rootDir: string): Thenable<any> {
134134
return Promise.reject(null);
135135
}
136136
if (item === yes) {
137-
return promptToEnableAndConfigureTestFramework();
137+
return promptToEnableAndConfigureTestFramework(outputChannel);
138138
}
139139
else {
140140
const pythonConfig = vscode.workspace.getConfiguration('python');

src/client/unittests/main.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ let testResultDisplay: TestResultDisplay;
2222
let testDisplay: TestDisplay;
2323
let outChannel: vscode.OutputChannel;
2424

25-
export function activate(context: vscode.ExtensionContext, outputChannel: vscode.OutputChannel) {
25+
export function activate(context: vscode.ExtensionContext, outputChannel: vscode.OutputChannel) {
2626
context.subscriptions.push({ dispose: dispose });
2727
outChannel = outputChannel;
2828
let disposables = registerCommands();
@@ -36,7 +36,7 @@ export function activate(context: vscode.ExtensionContext, outputChannel: vscode
3636
settings.addListener('change', onConfigChanged);
3737
context.subscriptions.push(activateCodeLenses());
3838

39-
displayPromptToEnableTests(vscode.workspace.rootPath);
39+
displayPromptToEnableTests(vscode.workspace.rootPath, outChannel);
4040
}
4141
function dispose() {
4242
if (pyTestManager) {
@@ -72,7 +72,7 @@ function registerCommands(): vscode.Disposable[] {
7272
function displayUI() {
7373
let testManager = getTestRunner();
7474
if (!testManager) {
75-
return displayTestFrameworkError();
75+
return displayTestFrameworkError(outChannel);
7676
}
7777

7878
testDisplay = testDisplay ? testDisplay : new TestDisplay();
@@ -81,7 +81,7 @@ function displayUI() {
8181
function displayPickerUI(file: string, testFunctions: TestFunction[]) {
8282
let testManager = getTestRunner();
8383
if (!testManager) {
84-
return displayTestFrameworkError();
84+
return displayTestFrameworkError(outChannel);
8585
}
8686

8787
testDisplay = testDisplay ? testDisplay : new TestDisplay();
@@ -90,7 +90,7 @@ function displayPickerUI(file: string, testFunctions: TestFunction[]) {
9090
function selectAndRunTestMethod() {
9191
let testManager = getTestRunner();
9292
if (!testManager) {
93-
return displayTestFrameworkError();
93+
return displayTestFrameworkError(outChannel);
9494
}
9595
testManager.discoverTests(true, true).then(() => {
9696
const tests = getDiscoveredTests();
@@ -103,7 +103,7 @@ function selectAndRunTestMethod() {
103103
function displayStopUI(message: string) {
104104
let testManager = getTestRunner();
105105
if (!testManager) {
106-
return displayTestFrameworkError();
106+
return displayTestFrameworkError(outChannel);
107107
}
108108

109109
testDisplay = testDisplay ? testDisplay : new TestDisplay();
@@ -175,7 +175,7 @@ function discoverTests(ignoreCache?: boolean, quietMode: boolean = false) {
175175
let testManager = getTestRunner();
176176
if (!testManager) {
177177
if (!quietMode) {
178-
displayTestFrameworkError();
178+
displayTestFrameworkError(outChannel);
179179
}
180180
return Promise.resolve(null);
181181
}
@@ -225,7 +225,7 @@ function identifyTestType(rootDirectory: string, arg?: vscode.Uri | TestsToRun |
225225
function runTestsImpl(arg?: vscode.Uri | TestsToRun | boolean | FlattenedTestFunction) {
226226
let testManager = getTestRunner();
227227
if (!testManager) {
228-
return displayTestFrameworkError();
228+
return displayTestFrameworkError(outChannel);
229229
}
230230

231231
// lastRanTests = testsToRun;

src/client/unittests/pytest/testConfigurationManager.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as vscode from 'vscode';
22
import { TestConfigurationManager } from '../common/testConfigurationManager';
33
import * as fs from 'fs';
44
import * as path from 'path';
5+
import { Installer, Product } from '../../common/installer';
56

67
export class ConfigurationManager extends TestConfigurationManager {
78
public enable(): Thenable<any> {
@@ -32,6 +33,7 @@ export class ConfigurationManager extends TestConfigurationManager {
3233
const args = [];
3334
const configFileOptionLabel = 'Use existing config file';
3435
const options: vscode.QuickPickItem[] = [];
36+
let installer = new Installer(this.outputChannel);
3537
return ConfigurationManager.configFilesExist(rootDir).then(configExists => {
3638
if (configExists) {
3739
options.push({
@@ -47,6 +49,12 @@ export class ConfigurationManager extends TestConfigurationManager {
4749
if (typeof testDir === 'string' && testDir !== configFileOptionLabel) {
4850
args.push(testDir);
4951
}
52+
}).then(() => {
53+
return installer.isProductInstalled(Product.pytest);
54+
}).then(installed => {
55+
if (!installed){
56+
return installer.installProduct(Product.pytest);
57+
}
5058
}).then(() => {
5159
const pythonConfig = vscode.workspace.getConfiguration('python');
5260
return pythonConfig.update('unitTest.pyTestArgs', args);

0 commit comments

Comments
 (0)