Skip to content

Commit 807b9fe

Browse files
authored
Add create environment button to requirements.txt and pyproject.toml files (#20879)
Closes #20812 Related #20133
1 parent 730df28 commit 807b9fe

File tree

7 files changed

+291
-2
lines changed

7 files changed

+291
-2
lines changed

package.json

+14-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
},
1919
"publisher": "ms-python",
2020
"enabledApiProposals": [
21+
"contribEditorContentMenu",
2122
"quickPickSortByLabel",
2223
"envShellEvent",
2324
"testObserver"
@@ -41,7 +42,7 @@
4142
"theme": "dark"
4243
},
4344
"engines": {
44-
"vscode": "^1.76.0"
45+
"vscode": "^1.77.0-20230309"
4546
},
4647
"keywords": [
4748
"python",
@@ -1688,6 +1689,18 @@
16881689
"when": "!virtualWorkspace && shellExecutionSupported"
16891690
}
16901691
],
1692+
"editor/content": [
1693+
{
1694+
"group": "Python",
1695+
"command": "python.createEnvironment",
1696+
"when": "resourceLangId == pip-requirements && !virtualWorkspace && shellExecutionSupported"
1697+
},
1698+
{
1699+
"group": "Python",
1700+
"command": "python.createEnvironment",
1701+
"when": "resourceFilename == pyproject.toml && pipInstallableToml && !virtualWorkspace && shellExecutionSupported"
1702+
}
1703+
],
16911704
"editor/context": [
16921705
{
16931706
"command": "python.execInTerminal",

package.nls.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"python.command.python.sortImports.title": "Sort Imports",
33
"python.command.python.startREPL.title": "Start REPL",
4-
"python.command.python.createEnvironment.title": "Create Environment",
4+
"python.command.python.createEnvironment.title": "Create Environment...",
55
"python.command.python.createNewFile.title": "New Python File",
66
"python.command.python.createTerminal.title": "Create Terminal",
77
"python.command.python.execInTerminal.title": "Run Python File in Terminal",

src/client/common/vscodeApis/workspaceApis.ts

+12
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,15 @@ export function onDidSaveTextDocument(
4343
): vscode.Disposable {
4444
return vscode.workspace.onDidSaveTextDocument(listener, thisArgs, disposables);
4545
}
46+
47+
export function getOpenTextDocuments(): readonly vscode.TextDocument[] {
48+
return vscode.workspace.textDocuments;
49+
}
50+
51+
export function onDidOpenTextDocument(handler: (doc: vscode.TextDocument) => void): vscode.Disposable {
52+
return vscode.workspace.onDidOpenTextDocument(handler);
53+
}
54+
55+
export function onDidChangeTextDocument(handler: (e: vscode.TextDocumentChangeEvent) => void): vscode.Disposable {
56+
return vscode.workspace.onDidChangeTextDocument(handler);
57+
}

src/client/extensionActivation.ts

+2
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ import { DynamicPythonDebugConfigurationService } from './debugger/extension/con
6363
import { registerCreateEnvironmentFeatures } from './pythonEnvironments/creation/createEnvApi';
6464
import { IInterpreterQuickPick } from './interpreter/configuration/types';
6565
import { registerInstallFormatterPrompt } from './providers/prompts/installFormatterPrompt';
66+
import { registerPyProjectTomlCreateEnvFeatures } from './pythonEnvironments/creation/pyprojectTomlCreateEnv';
6667

6768
export async function activateComponents(
6869
// `ext` is passed to any extra activation funcs.
@@ -106,6 +107,7 @@ export function activateFeatures(ext: ExtensionState, _components: Components):
106107
);
107108
const pathUtils = ext.legacyIOC.serviceContainer.get<IPathUtils>(IPathUtils);
108109
registerCreateEnvironmentFeatures(ext.disposables, interpreterQuickPick, interpreterPathService, pathUtils);
110+
registerPyProjectTomlCreateEnvFeatures(ext.disposables);
109111
}
110112

111113
/// //////////////////////////

src/client/pythonEnvironments/creation/provider/venvUtils.ts

+5
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,11 @@ async function pickRequirementsFiles(files: string[], token?: CancellationToken)
101101
return undefined;
102102
}
103103

104+
export function isPipInstallableToml(tomlContent: string): boolean {
105+
const toml = tomlParse(tomlContent);
106+
return tomlHasBuildSystem(toml);
107+
}
108+
104109
export interface IPackageInstallSelection {
105110
installType: 'toml' | 'requirements' | 'none';
106111
installItem?: string;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import { TextDocument, TextDocumentChangeEvent } from 'vscode';
5+
import { IDisposableRegistry } from '../../common/types';
6+
import { executeCommand } from '../../common/vscodeApis/commandApis';
7+
import {
8+
onDidOpenTextDocument,
9+
onDidChangeTextDocument,
10+
getOpenTextDocuments,
11+
} from '../../common/vscodeApis/workspaceApis';
12+
import { isPipInstallableToml } from './provider/venvUtils';
13+
14+
async function setPyProjectTomlContextKey(doc: TextDocument): Promise<void> {
15+
if (isPipInstallableToml(doc.getText())) {
16+
await executeCommand('setContext', 'pipInstallableToml', true);
17+
} else {
18+
await executeCommand('setContext', 'pipInstallableToml', false);
19+
}
20+
}
21+
22+
export function registerPyProjectTomlCreateEnvFeatures(disposables: IDisposableRegistry): void {
23+
disposables.push(
24+
onDidOpenTextDocument(async (doc: TextDocument) => {
25+
if (doc.fileName.endsWith('pyproject.toml')) {
26+
await setPyProjectTomlContextKey(doc);
27+
}
28+
}),
29+
onDidChangeTextDocument(async (e: TextDocumentChangeEvent) => {
30+
if (e.document.fileName.endsWith('pyproject.toml')) {
31+
await setPyProjectTomlContextKey(e.document);
32+
}
33+
}),
34+
);
35+
36+
getOpenTextDocuments().forEach(async (doc: TextDocument) => {
37+
if (doc.fileName.endsWith('pyproject.toml')) {
38+
await setPyProjectTomlContextKey(doc);
39+
}
40+
});
41+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
/* eslint-disable @typescript-eslint/no-explicit-any */
4+
5+
import * as chaiAsPromised from 'chai-as-promised';
6+
import * as sinon from 'sinon';
7+
import * as typemoq from 'typemoq';
8+
import { assert, use as chaiUse } from 'chai';
9+
import { TextDocument, TextDocumentChangeEvent } from 'vscode';
10+
import * as cmdApis from '../../../client/common/vscodeApis/commandApis';
11+
import * as workspaceApis from '../../../client/common/vscodeApis/workspaceApis';
12+
import { IDisposableRegistry } from '../../../client/common/types';
13+
import { registerPyProjectTomlCreateEnvFeatures } from '../../../client/pythonEnvironments/creation/pyprojectTomlCreateEnv';
14+
15+
chaiUse(chaiAsPromised);
16+
17+
class FakeDisposable {
18+
public dispose() {
19+
// Do nothing
20+
}
21+
}
22+
23+
function getInstallableToml(): typemoq.IMock<TextDocument> {
24+
const pyprojectTomlPath = 'pyproject.toml';
25+
const pyprojectToml = typemoq.Mock.ofType<TextDocument>();
26+
pyprojectToml.setup((p) => p.fileName).returns(() => pyprojectTomlPath);
27+
pyprojectToml
28+
.setup((p) => p.getText(typemoq.It.isAny()))
29+
.returns(
30+
() =>
31+
'[project]\nname = "spam"\nversion = "2020.0.0"\n[build-system]\nrequires = ["setuptools ~= 58.0", "cython ~= 0.29.0"]\n[project.optional-dependencies]\ntest = ["pytest"]\ndoc = ["sphinx", "furo"]',
32+
);
33+
return pyprojectToml;
34+
}
35+
36+
function getNonInstallableToml(): typemoq.IMock<TextDocument> {
37+
const pyprojectTomlPath = 'pyproject.toml';
38+
const pyprojectToml = typemoq.Mock.ofType<TextDocument>();
39+
pyprojectToml.setup((p) => p.fileName).returns(() => pyprojectTomlPath);
40+
pyprojectToml
41+
.setup((p) => p.getText(typemoq.It.isAny()))
42+
.returns(() => '[project]\nname = "spam"\nversion = "2020.0.0"\n');
43+
return pyprojectToml;
44+
}
45+
46+
function getSomeFile(): typemoq.IMock<TextDocument> {
47+
const someFilePath = 'something.py';
48+
const someFile = typemoq.Mock.ofType<TextDocument>();
49+
someFile.setup((p) => p.fileName).returns(() => someFilePath);
50+
someFile.setup((p) => p.getText(typemoq.It.isAny())).returns(() => 'print("Hello World")');
51+
return someFile;
52+
}
53+
54+
suite('PyProject.toml Create Env Features', () => {
55+
let executeCommandStub: sinon.SinonStub;
56+
const disposables: IDisposableRegistry = [];
57+
let getOpenTextDocumentsStub: sinon.SinonStub;
58+
let onDidOpenTextDocumentStub: sinon.SinonStub;
59+
let onDidChangeTextDocumentStub: sinon.SinonStub;
60+
61+
setup(() => {
62+
executeCommandStub = sinon.stub(cmdApis, 'executeCommand');
63+
getOpenTextDocumentsStub = sinon.stub(workspaceApis, 'getOpenTextDocuments');
64+
onDidOpenTextDocumentStub = sinon.stub(workspaceApis, 'onDidOpenTextDocument');
65+
onDidChangeTextDocumentStub = sinon.stub(workspaceApis, 'onDidChangeTextDocument');
66+
67+
onDidOpenTextDocumentStub.returns(new FakeDisposable());
68+
onDidChangeTextDocumentStub.returns(new FakeDisposable());
69+
});
70+
71+
teardown(() => {
72+
sinon.restore();
73+
disposables.forEach((d) => d.dispose());
74+
});
75+
76+
test('Installable pyproject.toml is already open in the editor on extension activate', async () => {
77+
const pyprojectToml = getInstallableToml();
78+
getOpenTextDocumentsStub.returns([pyprojectToml.object]);
79+
80+
registerPyProjectTomlCreateEnvFeatures(disposables);
81+
82+
assert.ok(executeCommandStub.calledOnceWithExactly('setContext', 'pipInstallableToml', true));
83+
});
84+
85+
test('Non installable pyproject.toml is already open in the editor on extension activate', async () => {
86+
const pyprojectToml = getNonInstallableToml();
87+
getOpenTextDocumentsStub.returns([pyprojectToml.object]);
88+
89+
registerPyProjectTomlCreateEnvFeatures(disposables);
90+
91+
assert.ok(executeCommandStub.calledOnceWithExactly('setContext', 'pipInstallableToml', false));
92+
});
93+
94+
test('Some random file open in the editor on extension activate', async () => {
95+
const someFile = getSomeFile();
96+
getOpenTextDocumentsStub.returns([someFile.object]);
97+
98+
registerPyProjectTomlCreateEnvFeatures(disposables);
99+
100+
assert.ok(executeCommandStub.notCalled);
101+
});
102+
103+
test('Installable pyproject.toml is opened in the editor', async () => {
104+
getOpenTextDocumentsStub.returns([]);
105+
106+
let handler: (doc: TextDocument) => void = () => {
107+
/* do nothing */
108+
};
109+
onDidOpenTextDocumentStub.callsFake((callback) => {
110+
handler = callback;
111+
return new FakeDisposable();
112+
});
113+
114+
const pyprojectToml = getInstallableToml();
115+
116+
registerPyProjectTomlCreateEnvFeatures(disposables);
117+
handler(pyprojectToml.object);
118+
119+
assert.ok(executeCommandStub.calledOnceWithExactly('setContext', 'pipInstallableToml', true));
120+
});
121+
122+
test('Non Installable pyproject.toml is opened in the editor', async () => {
123+
getOpenTextDocumentsStub.returns([]);
124+
125+
let handler: (doc: TextDocument) => void = () => {
126+
/* do nothing */
127+
};
128+
onDidOpenTextDocumentStub.callsFake((callback) => {
129+
handler = callback;
130+
return new FakeDisposable();
131+
});
132+
133+
const pyprojectToml = getNonInstallableToml();
134+
135+
registerPyProjectTomlCreateEnvFeatures(disposables);
136+
handler(pyprojectToml.object);
137+
138+
assert.ok(executeCommandStub.calledOnceWithExactly('setContext', 'pipInstallableToml', false));
139+
});
140+
141+
test('Some random file is opened in the editor', async () => {
142+
getOpenTextDocumentsStub.returns([]);
143+
144+
let handler: (doc: TextDocument) => void = () => {
145+
/* do nothing */
146+
};
147+
onDidOpenTextDocumentStub.callsFake((callback) => {
148+
handler = callback;
149+
return new FakeDisposable();
150+
});
151+
152+
const someFile = getSomeFile();
153+
154+
registerPyProjectTomlCreateEnvFeatures(disposables);
155+
handler(someFile.object);
156+
157+
assert.ok(executeCommandStub.notCalled);
158+
});
159+
160+
test('Installable pyproject.toml is changed', async () => {
161+
getOpenTextDocumentsStub.returns([]);
162+
163+
let handler: (d: TextDocumentChangeEvent) => void = () => {
164+
/* do nothing */
165+
};
166+
onDidChangeTextDocumentStub.callsFake((callback) => {
167+
handler = callback;
168+
return new FakeDisposable();
169+
});
170+
171+
const pyprojectToml = getInstallableToml();
172+
173+
registerPyProjectTomlCreateEnvFeatures(disposables);
174+
handler({ contentChanges: [], document: pyprojectToml.object, reason: undefined });
175+
176+
assert.ok(executeCommandStub.calledOnceWithExactly('setContext', 'pipInstallableToml', true));
177+
});
178+
179+
test('Non Installable pyproject.toml is changed', async () => {
180+
getOpenTextDocumentsStub.returns([]);
181+
182+
let handler: (d: TextDocumentChangeEvent) => void = () => {
183+
/* do nothing */
184+
};
185+
onDidChangeTextDocumentStub.callsFake((callback) => {
186+
handler = callback;
187+
return new FakeDisposable();
188+
});
189+
190+
const pyprojectToml = getNonInstallableToml();
191+
192+
registerPyProjectTomlCreateEnvFeatures(disposables);
193+
handler({ contentChanges: [], document: pyprojectToml.object, reason: undefined });
194+
195+
assert.ok(executeCommandStub.calledOnceWithExactly('setContext', 'pipInstallableToml', false));
196+
});
197+
198+
test('Some random file is changed', async () => {
199+
getOpenTextDocumentsStub.returns([]);
200+
201+
let handler: (d: TextDocumentChangeEvent) => void = () => {
202+
/* do nothing */
203+
};
204+
onDidChangeTextDocumentStub.callsFake((callback) => {
205+
handler = callback;
206+
return new FakeDisposable();
207+
});
208+
209+
const someFile = getSomeFile();
210+
211+
registerPyProjectTomlCreateEnvFeatures(disposables);
212+
handler({ contentChanges: [], document: someFile.object, reason: undefined });
213+
214+
assert.ok(executeCommandStub.notCalled);
215+
});
216+
});

0 commit comments

Comments
 (0)