Skip to content

Commit

Permalink
Add create environment button to requirements.txt and `pyproject.to…
Browse files Browse the repository at this point in the history
…ml` files (#20879)

Closes microsoft#20812
Related microsoft#20133
  • Loading branch information
karthiknadig committed Mar 20, 2023
1 parent 730df28 commit 807b9fe
Show file tree
Hide file tree
Showing 7 changed files with 291 additions and 2 deletions.
15 changes: 14 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
},
"publisher": "ms-python",
"enabledApiProposals": [
"contribEditorContentMenu",
"quickPickSortByLabel",
"envShellEvent",
"testObserver"
Expand All @@ -41,7 +42,7 @@
"theme": "dark"
},
"engines": {
"vscode": "^1.76.0"
"vscode": "^1.77.0-20230309"
},
"keywords": [
"python",
Expand Down Expand Up @@ -1688,6 +1689,18 @@
"when": "!virtualWorkspace && shellExecutionSupported"
}
],
"editor/content": [
{
"group": "Python",
"command": "python.createEnvironment",
"when": "resourceLangId == pip-requirements && !virtualWorkspace && shellExecutionSupported"
},
{
"group": "Python",
"command": "python.createEnvironment",
"when": "resourceFilename == pyproject.toml && pipInstallableToml && !virtualWorkspace && shellExecutionSupported"
}
],
"editor/context": [
{
"command": "python.execInTerminal",
Expand Down
2 changes: 1 addition & 1 deletion package.nls.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"python.command.python.sortImports.title": "Sort Imports",
"python.command.python.startREPL.title": "Start REPL",
"python.command.python.createEnvironment.title": "Create Environment",
"python.command.python.createEnvironment.title": "Create Environment...",
"python.command.python.createNewFile.title": "New Python File",
"python.command.python.createTerminal.title": "Create Terminal",
"python.command.python.execInTerminal.title": "Run Python File in Terminal",
Expand Down
12 changes: 12 additions & 0 deletions src/client/common/vscodeApis/workspaceApis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,15 @@ export function onDidSaveTextDocument(
): vscode.Disposable {
return vscode.workspace.onDidSaveTextDocument(listener, thisArgs, disposables);
}

export function getOpenTextDocuments(): readonly vscode.TextDocument[] {
return vscode.workspace.textDocuments;
}

export function onDidOpenTextDocument(handler: (doc: vscode.TextDocument) => void): vscode.Disposable {
return vscode.workspace.onDidOpenTextDocument(handler);
}

export function onDidChangeTextDocument(handler: (e: vscode.TextDocumentChangeEvent) => void): vscode.Disposable {
return vscode.workspace.onDidChangeTextDocument(handler);
}
2 changes: 2 additions & 0 deletions src/client/extensionActivation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ import { DynamicPythonDebugConfigurationService } from './debugger/extension/con
import { registerCreateEnvironmentFeatures } from './pythonEnvironments/creation/createEnvApi';
import { IInterpreterQuickPick } from './interpreter/configuration/types';
import { registerInstallFormatterPrompt } from './providers/prompts/installFormatterPrompt';
import { registerPyProjectTomlCreateEnvFeatures } from './pythonEnvironments/creation/pyprojectTomlCreateEnv';

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

/// //////////////////////////
Expand Down
5 changes: 5 additions & 0 deletions src/client/pythonEnvironments/creation/provider/venvUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,11 @@ async function pickRequirementsFiles(files: string[], token?: CancellationToken)
return undefined;
}

export function isPipInstallableToml(tomlContent: string): boolean {
const toml = tomlParse(tomlContent);
return tomlHasBuildSystem(toml);
}

export interface IPackageInstallSelection {
installType: 'toml' | 'requirements' | 'none';
installItem?: string;
Expand Down
41 changes: 41 additions & 0 deletions src/client/pythonEnvironments/creation/pyprojectTomlCreateEnv.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import { TextDocument, TextDocumentChangeEvent } from 'vscode';
import { IDisposableRegistry } from '../../common/types';
import { executeCommand } from '../../common/vscodeApis/commandApis';
import {
onDidOpenTextDocument,
onDidChangeTextDocument,
getOpenTextDocuments,
} from '../../common/vscodeApis/workspaceApis';
import { isPipInstallableToml } from './provider/venvUtils';

async function setPyProjectTomlContextKey(doc: TextDocument): Promise<void> {
if (isPipInstallableToml(doc.getText())) {
await executeCommand('setContext', 'pipInstallableToml', true);
} else {
await executeCommand('setContext', 'pipInstallableToml', false);
}
}

export function registerPyProjectTomlCreateEnvFeatures(disposables: IDisposableRegistry): void {
disposables.push(
onDidOpenTextDocument(async (doc: TextDocument) => {
if (doc.fileName.endsWith('pyproject.toml')) {
await setPyProjectTomlContextKey(doc);
}
}),
onDidChangeTextDocument(async (e: TextDocumentChangeEvent) => {
if (e.document.fileName.endsWith('pyproject.toml')) {
await setPyProjectTomlContextKey(e.document);
}
}),
);

getOpenTextDocuments().forEach(async (doc: TextDocument) => {
if (doc.fileName.endsWith('pyproject.toml')) {
await setPyProjectTomlContextKey(doc);
}
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
/* eslint-disable @typescript-eslint/no-explicit-any */

import * as chaiAsPromised from 'chai-as-promised';
import * as sinon from 'sinon';
import * as typemoq from 'typemoq';
import { assert, use as chaiUse } from 'chai';
import { TextDocument, TextDocumentChangeEvent } from 'vscode';
import * as cmdApis from '../../../client/common/vscodeApis/commandApis';
import * as workspaceApis from '../../../client/common/vscodeApis/workspaceApis';
import { IDisposableRegistry } from '../../../client/common/types';
import { registerPyProjectTomlCreateEnvFeatures } from '../../../client/pythonEnvironments/creation/pyprojectTomlCreateEnv';

chaiUse(chaiAsPromised);

class FakeDisposable {
public dispose() {
// Do nothing
}
}

function getInstallableToml(): typemoq.IMock<TextDocument> {
const pyprojectTomlPath = 'pyproject.toml';
const pyprojectToml = typemoq.Mock.ofType<TextDocument>();
pyprojectToml.setup((p) => p.fileName).returns(() => pyprojectTomlPath);
pyprojectToml
.setup((p) => p.getText(typemoq.It.isAny()))
.returns(
() =>
'[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"]',
);
return pyprojectToml;
}

function getNonInstallableToml(): typemoq.IMock<TextDocument> {
const pyprojectTomlPath = 'pyproject.toml';
const pyprojectToml = typemoq.Mock.ofType<TextDocument>();
pyprojectToml.setup((p) => p.fileName).returns(() => pyprojectTomlPath);
pyprojectToml
.setup((p) => p.getText(typemoq.It.isAny()))
.returns(() => '[project]\nname = "spam"\nversion = "2020.0.0"\n');
return pyprojectToml;
}

function getSomeFile(): typemoq.IMock<TextDocument> {
const someFilePath = 'something.py';
const someFile = typemoq.Mock.ofType<TextDocument>();
someFile.setup((p) => p.fileName).returns(() => someFilePath);
someFile.setup((p) => p.getText(typemoq.It.isAny())).returns(() => 'print("Hello World")');
return someFile;
}

suite('PyProject.toml Create Env Features', () => {
let executeCommandStub: sinon.SinonStub;
const disposables: IDisposableRegistry = [];
let getOpenTextDocumentsStub: sinon.SinonStub;
let onDidOpenTextDocumentStub: sinon.SinonStub;
let onDidChangeTextDocumentStub: sinon.SinonStub;

setup(() => {
executeCommandStub = sinon.stub(cmdApis, 'executeCommand');
getOpenTextDocumentsStub = sinon.stub(workspaceApis, 'getOpenTextDocuments');
onDidOpenTextDocumentStub = sinon.stub(workspaceApis, 'onDidOpenTextDocument');
onDidChangeTextDocumentStub = sinon.stub(workspaceApis, 'onDidChangeTextDocument');

onDidOpenTextDocumentStub.returns(new FakeDisposable());
onDidChangeTextDocumentStub.returns(new FakeDisposable());
});

teardown(() => {
sinon.restore();
disposables.forEach((d) => d.dispose());
});

test('Installable pyproject.toml is already open in the editor on extension activate', async () => {
const pyprojectToml = getInstallableToml();
getOpenTextDocumentsStub.returns([pyprojectToml.object]);

registerPyProjectTomlCreateEnvFeatures(disposables);

assert.ok(executeCommandStub.calledOnceWithExactly('setContext', 'pipInstallableToml', true));
});

test('Non installable pyproject.toml is already open in the editor on extension activate', async () => {
const pyprojectToml = getNonInstallableToml();
getOpenTextDocumentsStub.returns([pyprojectToml.object]);

registerPyProjectTomlCreateEnvFeatures(disposables);

assert.ok(executeCommandStub.calledOnceWithExactly('setContext', 'pipInstallableToml', false));
});

test('Some random file open in the editor on extension activate', async () => {
const someFile = getSomeFile();
getOpenTextDocumentsStub.returns([someFile.object]);

registerPyProjectTomlCreateEnvFeatures(disposables);

assert.ok(executeCommandStub.notCalled);
});

test('Installable pyproject.toml is opened in the editor', async () => {
getOpenTextDocumentsStub.returns([]);

let handler: (doc: TextDocument) => void = () => {
/* do nothing */
};
onDidOpenTextDocumentStub.callsFake((callback) => {
handler = callback;
return new FakeDisposable();
});

const pyprojectToml = getInstallableToml();

registerPyProjectTomlCreateEnvFeatures(disposables);
handler(pyprojectToml.object);

assert.ok(executeCommandStub.calledOnceWithExactly('setContext', 'pipInstallableToml', true));
});

test('Non Installable pyproject.toml is opened in the editor', async () => {
getOpenTextDocumentsStub.returns([]);

let handler: (doc: TextDocument) => void = () => {
/* do nothing */
};
onDidOpenTextDocumentStub.callsFake((callback) => {
handler = callback;
return new FakeDisposable();
});

const pyprojectToml = getNonInstallableToml();

registerPyProjectTomlCreateEnvFeatures(disposables);
handler(pyprojectToml.object);

assert.ok(executeCommandStub.calledOnceWithExactly('setContext', 'pipInstallableToml', false));
});

test('Some random file is opened in the editor', async () => {
getOpenTextDocumentsStub.returns([]);

let handler: (doc: TextDocument) => void = () => {
/* do nothing */
};
onDidOpenTextDocumentStub.callsFake((callback) => {
handler = callback;
return new FakeDisposable();
});

const someFile = getSomeFile();

registerPyProjectTomlCreateEnvFeatures(disposables);
handler(someFile.object);

assert.ok(executeCommandStub.notCalled);
});

test('Installable pyproject.toml is changed', async () => {
getOpenTextDocumentsStub.returns([]);

let handler: (d: TextDocumentChangeEvent) => void = () => {
/* do nothing */
};
onDidChangeTextDocumentStub.callsFake((callback) => {
handler = callback;
return new FakeDisposable();
});

const pyprojectToml = getInstallableToml();

registerPyProjectTomlCreateEnvFeatures(disposables);
handler({ contentChanges: [], document: pyprojectToml.object, reason: undefined });

assert.ok(executeCommandStub.calledOnceWithExactly('setContext', 'pipInstallableToml', true));
});

test('Non Installable pyproject.toml is changed', async () => {
getOpenTextDocumentsStub.returns([]);

let handler: (d: TextDocumentChangeEvent) => void = () => {
/* do nothing */
};
onDidChangeTextDocumentStub.callsFake((callback) => {
handler = callback;
return new FakeDisposable();
});

const pyprojectToml = getNonInstallableToml();

registerPyProjectTomlCreateEnvFeatures(disposables);
handler({ contentChanges: [], document: pyprojectToml.object, reason: undefined });

assert.ok(executeCommandStub.calledOnceWithExactly('setContext', 'pipInstallableToml', false));
});

test('Some random file is changed', async () => {
getOpenTextDocumentsStub.returns([]);

let handler: (d: TextDocumentChangeEvent) => void = () => {
/* do nothing */
};
onDidChangeTextDocumentStub.callsFake((callback) => {
handler = callback;
return new FakeDisposable();
});

const someFile = getSomeFile();

registerPyProjectTomlCreateEnvFeatures(disposables);
handler({ contentChanges: [], document: someFile.object, reason: undefined });

assert.ok(executeCommandStub.notCalled);
});
});

1 comment on commit 807b9fe

@JonKsander1984
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Хороший шаблон для личного блога

Please sign in to comment.