Skip to content

Commit 508d98f

Browse files
Improve debug config in flask (#276)
* Add flask config * fix test and strings * fix lint * fix lint * Update src/extension/common/utils/localize.ts Co-authored-by: Luciana Abud <45497113+luabud@users.noreply.github.com> * clean code --------- Co-authored-by: Luciana Abud <45497113+luabud@users.noreply.github.com>
1 parent c6a7846 commit 508d98f

File tree

6 files changed

+205
-109
lines changed

6 files changed

+205
-109
lines changed

src/extension/common/utils/localize.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,7 @@ export namespace DebugConfigStrings {
100100
};
101101
export const djangoConfigPromp = {
102102
title: l10n.t('Debug Django'),
103-
prompt: l10n.t(
104-
"Enter the path to manage.py or select a file from the list ('${workspaceFolderToken}' points to the root of the current workspace folder)",
105-
),
103+
prompt: l10n.t('Enter the path to manage.py or select a file from the list.'),
106104
};
107105
}
108106
export namespace fastapi {
@@ -132,6 +130,10 @@ export namespace DebugConfigStrings {
132130
prompt: l10n.t('Python Debugger: Flask'),
133131
invalid: l10n.t('Enter a valid name'),
134132
};
133+
export const flaskConfigPromp = {
134+
title: l10n.t('Debug Flask'),
135+
prompt: l10n.t('Enter the path to app.py or select a file from the list.'),
136+
};
135137
}
136138
export namespace pyramid {
137139
export const snippet = {

src/extension/debugger/configuration/providers/flaskLaunch.ts

Lines changed: 27 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -5,70 +5,55 @@
55
'use strict';
66

77
import * as path from 'path';
8-
import * as fs from 'fs-extra';
9-
import { WorkspaceFolder } from 'vscode';
8+
import { Uri } from 'vscode';
109
import { DebugConfigStrings } from '../../../common/utils/localize';
1110
import { MultiStepInput } from '../../../common/multiStepInput';
12-
import { sendTelemetryEvent } from '../../../telemetry';
13-
import { EventName } from '../../../telemetry/constants';
1411
import { DebuggerTypeName } from '../../../constants';
1512
import { LaunchRequestArguments } from '../../../types';
16-
import { DebugConfigurationState, DebugConfigurationType } from '../../types';
13+
import { DebugConfigurationState } from '../../types';
14+
import { getFlaskPaths } from '../utils/configuration';
15+
import { QuickPickType } from './providerQuickPick/types';
16+
import { goToFileButton } from './providerQuickPick/providerQuickPick';
17+
import { parseFlaskPath, pickFlaskPrompt } from './providerQuickPick/flaskProviderQuickPick';
1718

1819
export async function buildFlaskLaunchDebugConfiguration(
1920
input: MultiStepInput<DebugConfigurationState>,
2021
state: DebugConfigurationState,
2122
): Promise<void> {
22-
const application = await getApplicationPath(state.folder);
23-
let manuallyEnteredAValue: boolean | undefined;
23+
let flaskPaths = await getFlaskPaths(state.folder);
24+
let options: QuickPickType[] = [];
25+
2426
const config: Partial<LaunchRequestArguments> = {
2527
name: DebugConfigStrings.flask.snippet.name,
2628
type: DebuggerTypeName,
2729
request: 'launch',
2830
module: 'flask',
2931
env: {
30-
FLASK_APP: application || 'app.py',
32+
FLASK_APP: 'app.py',
3133
FLASK_DEBUG: '1',
3234
},
3335
args: ['run', '--no-debugger', '--no-reload'],
3436
jinja: true,
3537
autoStartBrowser: false,
3638
};
3739

38-
if (!application) {
39-
const selectedApp = await input.showInputBox({
40-
title: DebugConfigStrings.flask.enterAppPathOrNamePath.title,
41-
value: 'app.py',
42-
prompt: DebugConfigStrings.flask.enterAppPathOrNamePath.prompt,
43-
validate: (value) =>
44-
Promise.resolve(
45-
value && value.trim().length > 0
46-
? undefined
47-
: DebugConfigStrings.flask.enterAppPathOrNamePath.invalid,
48-
),
40+
//add found paths to options
41+
if (flaskPaths.length > 0) {
42+
options.push(
43+
...flaskPaths.map((item) => ({
44+
label: path.basename(item.fsPath),
45+
filePath: item,
46+
description: parseFlaskPath(state.folder, item.fsPath),
47+
buttons: [goToFileButton],
48+
})),
49+
);
50+
} else {
51+
const managePath = path.join(state?.folder?.uri.fsPath || '', 'app.py');
52+
options.push({
53+
label: 'Default',
54+
description: parseFlaskPath(state.folder, managePath),
55+
filePath: Uri.file(managePath),
4956
});
50-
if (selectedApp) {
51-
manuallyEnteredAValue = true;
52-
config.env!.FLASK_APP = selectedApp;
53-
} else {
54-
return;
55-
}
56-
}
57-
58-
sendTelemetryEvent(EventName.DEBUGGER_CONFIGURATION_PROMPTS, undefined, {
59-
configurationType: DebugConfigurationType.launchFlask,
60-
autoDetectedFlaskAppPyPath: !!application,
61-
manuallyEnteredAValue,
62-
});
63-
Object.assign(state.config, config);
64-
}
65-
export async function getApplicationPath(folder: WorkspaceFolder | undefined): Promise<string | undefined> {
66-
if (!folder) {
67-
return undefined;
68-
}
69-
const defaultLocationOfManagePy = path.join(folder.uri.fsPath, 'app.py');
70-
if (await fs.pathExists(defaultLocationOfManagePy)) {
71-
return 'app.py';
7257
}
73-
return undefined;
58+
await input.run((_input, state) => pickFlaskPrompt(input, state, config, options), state);
7459
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import * as path from 'path';
5+
import { window, QuickPickItemButtonEvent, QuickPickItemKind, WorkspaceFolder } from 'vscode';
6+
import { IQuickPickParameters, InputFlowAction, MultiStepInput } from '../../../../common/multiStepInput';
7+
import { LaunchRequestArguments } from '../../../../types';
8+
import { DebugConfigurationState, DebugConfigurationType } from '../../../types';
9+
import { QuickPickType } from './types';
10+
import { browseFileOption, openFileExplorer } from './providerQuickPick';
11+
import { DebugConfigStrings } from '../../../../common/utils/localize';
12+
import { sendTelemetryEvent } from '../../../../telemetry';
13+
import { EventName } from '../../../../telemetry/constants';
14+
15+
export async function pickFlaskPrompt(
16+
input: MultiStepInput<DebugConfigurationState>,
17+
state: DebugConfigurationState,
18+
config: Partial<LaunchRequestArguments>,
19+
pathsOptions: QuickPickType[],
20+
) {
21+
let options: QuickPickType[] = [
22+
...pathsOptions,
23+
{ label: '', kind: QuickPickItemKind.Separator },
24+
browseFileOption,
25+
];
26+
27+
const selection = await input.showQuickPick<QuickPickType, IQuickPickParameters<QuickPickType>>({
28+
placeholder: DebugConfigStrings.flask.flaskConfigPromp.prompt,
29+
items: options,
30+
acceptFilterBoxTextAsSelection: true,
31+
activeItem: options[0],
32+
matchOnDescription: true,
33+
title: DebugConfigStrings.flask.flaskConfigPromp.title,
34+
onDidTriggerItemButton: async (e: QuickPickItemButtonEvent<QuickPickType>) => {
35+
if (e.item && 'filePath' in e.item) {
36+
await window.showTextDocument(e.item.filePath, { preview: true });
37+
}
38+
},
39+
});
40+
41+
if (selection === undefined) {
42+
return;
43+
} else if (selection.label === browseFileOption.label) {
44+
const uris = await openFileExplorer(state.folder?.uri);
45+
if (uris && uris.length > 0) {
46+
config.env!.FLASK_APP = parseFlaskPath(state.folder, uris[0].fsPath);
47+
sendTelemetryEvent(EventName.DEBUGGER_CONFIGURATION_PROMPTS, undefined, {
48+
configurationType: DebugConfigurationType.launchFlask,
49+
browsefilevalue: true,
50+
});
51+
} else {
52+
return Promise.reject(InputFlowAction.resume);
53+
}
54+
} else if (typeof selection === 'string') {
55+
config.env!.FLASK_APP = selection;
56+
sendTelemetryEvent(EventName.DEBUGGER_CONFIGURATION_PROMPTS, undefined, {
57+
configurationType: DebugConfigurationType.launchFlask,
58+
manuallyEnteredAValue: true,
59+
});
60+
} else {
61+
config.env!.FLASK_APP = selection.description;
62+
sendTelemetryEvent(EventName.DEBUGGER_CONFIGURATION_PROMPTS, undefined, {
63+
configurationType: DebugConfigurationType.launchFlask,
64+
autoDetectedFlaskAppPyPath: true,
65+
});
66+
}
67+
Object.assign(state.config, config);
68+
}
69+
70+
export function parseFlaskPath(folder: WorkspaceFolder | undefined, flaskPath: string): string | undefined {
71+
if (!folder) {
72+
return flaskPath;
73+
}
74+
const baseManagePath = path.relative(folder.uri.fsPath, flaskPath);
75+
if (baseManagePath && !baseManagePath.startsWith('..')) {
76+
return baseManagePath;
77+
} else {
78+
return flaskPath;
79+
}
80+
}

src/extension/debugger/configuration/utils/configuration.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ export async function getDjangoPaths(folder: WorkspaceFolder | undefined): Promi
7070

7171
export async function getFastApiPaths(folder: WorkspaceFolder | undefined) {
7272
if (!folder) {
73-
return undefined;
73+
return [];
7474
}
7575
const regExpression = /app\s*=\s*FastAPI\(/;
7676
const fastApiPaths = await getPossiblePaths(
@@ -83,7 +83,7 @@ export async function getFastApiPaths(folder: WorkspaceFolder | undefined) {
8383

8484
export async function getFlaskPaths(folder: WorkspaceFolder | undefined) {
8585
if (!folder) {
86-
return undefined;
86+
return [];
8787
}
8888
const regExpression = /app(?:lication)?\s*=\s*(?:flask\.)?Flask\(|def\s+(?:create|make)_app\(/;
8989
const flaskPaths = await getPossiblePaths(

src/test/unittest/configuration/providers/flaskLaunch.unit.test.ts

Lines changed: 46 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -5,87 +5,71 @@
55

66
import { expect } from 'chai';
77
import * as path from 'path';
8-
import * as fs from 'fs-extra';
98
import * as sinon from 'sinon';
10-
import { anything, instance, mock, when } from 'ts-mockito';
11-
import { Uri } from 'vscode';
12-
import { DebugConfigStrings } from '../../../../extension/common/utils/localize';
13-
import { DebuggerTypeName } from '../../../../extension/constants';
9+
import * as typemoq from 'typemoq';
10+
import { ThemeIcon, Uri } from 'vscode';
1411
import { DebugConfigurationState } from '../../../../extension/debugger/types';
1512
import * as flaskLaunch from '../../../../extension/debugger/configuration/providers/flaskLaunch';
1613
import { MultiStepInput } from '../../../../extension/common/multiStepInput';
14+
import * as configuration from '../../../../extension/debugger/configuration/utils/configuration';
15+
import * as flaskProviderQuickPick from '../../../../extension/debugger/configuration/providers/providerQuickPick/flaskProviderQuickPick';
1716

1817
suite('Debugging - Configuration Provider Flask', () => {
19-
let pathExistsStub: sinon.SinonStub;
20-
let input: MultiStepInput<DebugConfigurationState>;
18+
let multiStepInput: typemoq.IMock<MultiStepInput<DebugConfigurationState>>;
19+
let getFlaskPathsStub: sinon.SinonStub;
20+
let pickFlaskPromptStub: sinon.SinonStub;
21+
2122
setup(() => {
22-
input = mock<MultiStepInput<DebugConfigurationState>>(MultiStepInput);
23-
pathExistsStub = sinon.stub(fs, 'pathExists');
23+
multiStepInput = typemoq.Mock.ofType<MultiStepInput<DebugConfigurationState>>();
24+
multiStepInput
25+
.setup((i) => i.run(typemoq.It.isAny(), typemoq.It.isAny()))
26+
.returns((callback, _state) => callback());
27+
getFlaskPathsStub = sinon.stub(configuration, 'getFlaskPaths');
28+
pickFlaskPromptStub = sinon.stub(flaskProviderQuickPick, 'pickFlaskPrompt');
2429
});
2530
teardown(() => {
2631
sinon.restore();
2732
});
28-
test("getApplicationPath should return undefined if file doesn't exist", async () => {
29-
const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 };
30-
const appPyPath = path.join(folder.uri.fsPath, 'app.py');
31-
pathExistsStub.withArgs(appPyPath).resolves(false);
32-
const file = await flaskLaunch.getApplicationPath(folder);
33-
34-
expect(file).to.be.equal(undefined, 'Should return undefined');
35-
});
36-
test('getApplicationPath should file path', async () => {
37-
const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 };
38-
const appPyPath = path.join(folder.uri.fsPath, 'app.py');
39-
pathExistsStub.withArgs(appPyPath).resolves(true);
40-
const file = await flaskLaunch.getApplicationPath(folder);
41-
42-
expect(file).to.be.equal('app.py');
43-
});
44-
test('Launch JSON with selected app path', async () => {
33+
test('Show picker and send parsed found flask paths', async () => {
4534
const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 };
4635
const state = { config: {}, folder };
47-
48-
when(input.showInputBox(anything())).thenResolve('hello');
49-
50-
await flaskLaunch.buildFlaskLaunchDebugConfiguration(instance(input), state);
51-
52-
const config = {
53-
name: DebugConfigStrings.flask.snippet.name,
54-
type: DebuggerTypeName,
55-
request: 'launch',
56-
module: 'flask',
57-
env: {
58-
FLASK_APP: 'hello',
59-
FLASK_DEBUG: '1',
36+
const appPath = Uri.file(path.join(folder.uri.fsPath, 'app.py'));
37+
getFlaskPathsStub.resolves([appPath]);
38+
pickFlaskPromptStub.resolves();
39+
await flaskLaunch.buildFlaskLaunchDebugConfiguration(multiStepInput.object, state);
40+
const options = pickFlaskPromptStub.getCall(0).args[3];
41+
const expectedOptions = [
42+
{
43+
label: path.basename(appPath.fsPath),
44+
filePath: appPath,
45+
description: 'app.py',
46+
buttons: [
47+
{
48+
iconPath: new ThemeIcon('go-to-file'),
49+
tooltip: `Open in Preview`,
50+
},
51+
],
6052
},
61-
args: ['run', '--no-debugger', '--no-reload'],
62-
jinja: true,
63-
autoStartBrowser: false,
64-
};
53+
];
6554

66-
expect(state.config).to.be.deep.equal(config);
55+
expect(options).to.be.deep.equal(expectedOptions);
6756
});
68-
test('Launch JSON with default managepy path', async () => {
57+
test('Show picker and send default app.py path', async () => {
6958
const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 };
7059
const state = { config: {}, folder };
71-
when(input.showInputBox(anything())).thenResolve('app.py');
72-
73-
await flaskLaunch.buildFlaskLaunchDebugConfiguration(instance(input), state);
74-
75-
const config = {
76-
name: DebugConfigStrings.flask.snippet.name,
77-
type: DebuggerTypeName,
78-
request: 'launch',
79-
module: 'flask',
80-
env: {
81-
FLASK_APP: 'app.py',
82-
FLASK_DEBUG: '1',
60+
const appPath = path.join(state?.folder?.uri.fsPath, 'app.py');
61+
getFlaskPathsStub.resolves([]);
62+
pickFlaskPromptStub.resolves();
63+
await flaskLaunch.buildFlaskLaunchDebugConfiguration(multiStepInput.object, state);
64+
const options = pickFlaskPromptStub.getCall(0).args[3];
65+
const expectedOptions = [
66+
{
67+
label: 'Default',
68+
filePath: Uri.file(appPath),
69+
description: 'app.py',
8370
},
84-
args: ['run', '--no-debugger', '--no-reload'],
85-
jinja: true,
86-
autoStartBrowser: false,
87-
};
71+
];
8872

89-
expect(state.config).to.be.deep.equal(config);
73+
expect(options).to.be.deep.equal(expectedOptions);
9074
});
9175
});
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
'use strict';
5+
6+
import { Uri } from 'vscode';
7+
import { expect } from 'chai';
8+
import * as path from 'path';
9+
import * as typemoq from 'typemoq';
10+
import * as sinon from 'sinon';
11+
import { MultiStepInput } from '../../../../../extension/common/multiStepInput';
12+
import { DebugConfigurationState } from '../../../../../extension/debugger/types';
13+
import { parseFlaskPath } from '../../../../../extension/debugger/configuration/providers/providerQuickPick/flaskProviderQuickPick';
14+
15+
suite('Debugging - Configuration Provider Flask QuickPick', () => {
16+
let pathSeparatorStub: sinon.SinonStub;
17+
let multiStepInput: typemoq.IMock<MultiStepInput<DebugConfigurationState>>;
18+
19+
setup(() => {
20+
multiStepInput = typemoq.Mock.ofType<MultiStepInput<DebugConfigurationState>>();
21+
multiStepInput
22+
.setup((i) => i.run(typemoq.It.isAny(), typemoq.It.isAny()))
23+
.returns((callback, _state) => callback());
24+
pathSeparatorStub = sinon.stub(path, 'sep');
25+
pathSeparatorStub.value('-');
26+
});
27+
teardown(() => {
28+
sinon.restore();
29+
});
30+
test('parseManagePyPath should parse the path and return it with workspaceFolderToken', () => {
31+
const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 };
32+
const flaskPath = path.join(folder.uri.fsPath, 'app.py');
33+
const file = parseFlaskPath(folder, flaskPath);
34+
pathSeparatorStub.value('-');
35+
const expectedValue = `app.py`;
36+
expect(file).to.be.equal(expectedValue);
37+
});
38+
test('parseManagePyPath should return the same path if the workspace do not match', () => {
39+
const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 };
40+
const flaskPath = 'random/path/app.py';
41+
const file = parseFlaskPath(folder, flaskPath);
42+
43+
expect(file).to.be.equal(flaskPath);
44+
});
45+
});

0 commit comments

Comments
 (0)