Skip to content

Commit 06fc5bb

Browse files
Refactor for dev and cli detection
1 parent dcd8b80 commit 06fc5bb

7 files changed

Lines changed: 112 additions & 11 deletions

File tree

src/extension/common/utils/localize.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,13 @@ export namespace DebugConfigStrings {
111111
label: l10n.t('FastAPI'),
112112
description: l10n.t('Launch and debug a FastAPI web application'),
113113
};
114+
export const snippetFile = {
115+
name: l10n.t('Python Debugger: FastAPI File'),
116+
};
117+
export const selectConfigurationWithFile = {
118+
label: l10n.t('FastAPI File'),
119+
description: l10n.t('Launch and debug a FastAPI web application using the current file'),
120+
};
114121
}
115122
export namespace flask {
116123
export const snippet = {

src/extension/debugger/configuration/debugConfigurationService.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { IMultiStepInputFactory, InputStep, IQuickPickParameters, MultiStepInput
88
import { AttachRequestArguments, DebugConfigurationArguments, LaunchRequestArguments } from '../../types';
99
import { DebugConfigurationState, DebugConfigurationType, IDebugConfigurationService } from '../types';
1010
import { buildDjangoLaunchDebugConfiguration } from './providers/djangoLaunch';
11-
import { buildFastAPILaunchDebugConfiguration } from './providers/fastapiLaunch';
11+
import { buildFastAPILaunchDebugConfiguration, buildFastAPIWithFileLaunchDebugConfiguration } from './providers/fastapiLaunch';
1212
import { buildFileLaunchDebugConfiguration } from './providers/fileLaunch';
1313
import { buildFlaskLaunchDebugConfiguration } from './providers/flaskLaunch';
1414
import { buildModuleLaunchConfiguration } from './providers/moduleLaunch';
@@ -158,6 +158,11 @@ export class PythonDebugConfigurationService implements IDebugConfigurationServi
158158
type: DebugConfigurationType.launchFastAPI,
159159
description: DebugConfigStrings.fastapi.selectConfiguration.description,
160160
},
161+
{
162+
label: DebugConfigStrings.fastapi.selectConfigurationWithFile.label,
163+
type: DebugConfigurationType.launchFastAPIWithFile,
164+
description: DebugConfigStrings.fastapi.selectConfigurationWithFile.description,
165+
},
161166
{
162167
label: DebugConfigStrings.flask.selectConfiguration.label,
163168
type: DebugConfigurationType.launchFlask,
@@ -178,6 +183,7 @@ export class PythonDebugConfigurationService implements IDebugConfigurationServi
178183
>();
179184
debugConfigurations.set(DebugConfigurationType.launchDjango, buildDjangoLaunchDebugConfiguration);
180185
debugConfigurations.set(DebugConfigurationType.launchFastAPI, buildFastAPILaunchDebugConfiguration);
186+
debugConfigurations.set(DebugConfigurationType.launchFastAPIWithFile, buildFastAPIWithFileLaunchDebugConfiguration);
181187
debugConfigurations.set(DebugConfigurationType.launchFile, buildFileLaunchDebugConfiguration);
182188
debugConfigurations.set(DebugConfigurationType.launchFileWithArgs, buildFileWithArgsLaunchDebugConfiguration);
183189
debugConfigurations.set(DebugConfigurationType.launchFlask, buildFlaskLaunchDebugConfiguration);

src/extension/debugger/configuration/dynamicdebugConfigurationService.ts

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ import * as path from 'path';
88
import { CancellationToken, DebugConfiguration, WorkspaceFolder } from 'vscode';
99
import { IDynamicDebugConfigurationService } from '../types';
1010
import { DebuggerTypeName } from '../../constants';
11-
import { getDjangoPaths, getFastApiPaths, getFlaskPaths } from './utils/configuration';
11+
import { getDjangoPaths, getFastApiPaths, getFlaskPaths, isFastApiCliAvailable } from './utils/configuration';
1212
import { sendTelemetryEvent } from '../../telemetry';
1313
import { EventName } from '../../telemetry/constants';
14+
import { replaceAll } from '../../common/stringUtils';
1415

1516
const workspaceFolderToken = '${workspaceFolder}';
1617

@@ -63,13 +64,38 @@ export class DynamicPythonDebugConfigurationService implements IDynamicDebugConf
6364

6465
const fastApiPaths = await getFastApiPaths(folder);
6566
if (fastApiPaths?.length) {
66-
providers.push({
67-
name: 'Python Debugger: FastAPI',
68-
type: DebuggerTypeName,
69-
request: 'launch',
70-
module: 'fastapi',
71-
args: ['run'],
72-
});
67+
const hasFastApiCli = await isFastApiCliAvailable(folder.uri);
68+
if (hasFastApiCli) {
69+
providers.push({
70+
name: 'Python Debugger: FastAPI',
71+
type: DebuggerTypeName,
72+
request: 'launch',
73+
module: 'fastapi',
74+
args: ['dev'],
75+
jinja: true,
76+
subProcess: true,
77+
});
78+
providers.push({
79+
name: 'Python Debugger: FastAPI File',
80+
type: DebuggerTypeName,
81+
request: 'launch',
82+
module: 'fastapi',
83+
args: ['dev', '${file}'],
84+
jinja: true,
85+
subProcess: true,
86+
});
87+
} else {
88+
// Legacy fallback when fastapi-cli is not available.
89+
const fastApiPath = replaceAll(path.relative(folder.uri.fsPath, fastApiPaths[0].fsPath), path.sep, '.').replace('.py', '');
90+
providers.push({
91+
name: 'Python Debugger: FastAPI',
92+
type: DebuggerTypeName,
93+
request: 'launch',
94+
module: 'uvicorn',
95+
args: [`${fastApiPath}:app`, '--reload'],
96+
jinja: true,
97+
});
98+
}
7399
}
74100

75101
sendTelemetryEvent(EventName.DEBUGGER_DYNAMIC_CONFIGURATION, undefined, { providers: providers });

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

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,31 @@ export async function buildFastAPILaunchDebugConfiguration(
2020
type: DebuggerTypeName,
2121
request: 'launch',
2222
module: 'fastapi',
23-
args: ['run'],
23+
args: ['dev'],
24+
jinja: true,
25+
subProcess: true,
2426
};
2527
sendTelemetryEvent(EventName.DEBUGGER_CONFIGURATION_PROMPTS, undefined, {
2628
configurationType: DebugConfigurationType.launchFastAPI,
2729
});
2830
Object.assign(state.config, config);
2931
}
32+
33+
export async function buildFastAPIWithFileLaunchDebugConfiguration(
34+
_input: MultiStepInput<DebugConfigurationState>,
35+
state: DebugConfigurationState,
36+
): Promise<void> {
37+
const config: Partial<LaunchRequestArguments> = {
38+
name: DebugConfigStrings.fastapi.snippetFile.name,
39+
type: DebuggerTypeName,
40+
request: 'launch',
41+
module: 'fastapi',
42+
args: ['dev', '${file}'],
43+
jinja: true,
44+
subProcess: true,
45+
};
46+
sendTelemetryEvent(EventName.DEBUGGER_CONFIGURATION_PROMPTS, undefined, {
47+
configurationType: DebugConfigurationType.launchFastAPIWithFile,
48+
});
49+
Object.assign(state.config, config);
50+
}

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,11 @@ import { AttachRequestArguments } from '../../../types';
1515
import { DebugConfigurationState, DebugConfigurationType } from '../../types';
1616
import { Uri, WorkspaceFolder, workspace } from 'vscode';
1717
import { asyncFilter } from '../../../common/utilities';
18+
import { getInterpreterDetails } from '../../../common/python';
19+
import { plainExec } from '../../../common/process/rawProcessApis';
1820

1921
const defaultPort = 5678;
22+
const fastApiCliCache = new Map<string, Promise<boolean>>();
2023

2124
export async function configurePort(
2225
input: MultiStepInput<DebugConfigurationState>,
@@ -81,6 +84,22 @@ export async function getFastApiPaths(folder: WorkspaceFolder | undefined) {
8184
return fastApiPaths;
8285
}
8386

87+
export async function isFastApiCliAvailable(resource?: Uri): Promise<boolean> {
88+
const interpreterDetails = await getInterpreterDetails(resource);
89+
const pythonPath = interpreterDetails?.path?.[0];
90+
if (!pythonPath) {
91+
return false;
92+
}
93+
if (fastApiCliCache.has(pythonPath)) {
94+
return fastApiCliCache.get(pythonPath)!;
95+
}
96+
const promise = plainExec(pythonPath, ['-c', 'import fastapi_cli'], { throwOnStdErr: false })
97+
.then(() => true)
98+
.catch(() => false);
99+
fastApiCliCache.set(pythonPath, promise);
100+
return promise;
101+
}
102+
84103
export async function getFlaskPaths(folder: WorkspaceFolder | undefined) {
85104
if (!folder) {
86105
return [];

src/extension/debugger/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export enum DebugConfigurationType {
3434
remoteAttach = 'remoteAttach',
3535
launchDjango = 'launchDjango',
3636
launchFastAPI = 'launchFastAPI',
37+
launchFastAPIWithFile = 'launchFastAPIWithFile',
3738
launchFlask = 'launchFlask',
3839
launchModule = 'launchModule',
3940
launchPyramid = 'launchPyramid',

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

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,28 @@ suite('Debugging - Configuration Provider FastAPI', () => {
3131
type: DebuggerTypeName,
3232
request: 'launch',
3333
module: 'fastapi',
34-
args: ['run'],
34+
args: ['dev'],
35+
jinja: true,
36+
subProcess: true,
37+
};
38+
39+
expect(state.config).to.be.deep.equal(config);
40+
});
41+
42+
test('Launch JSON with file configuration', async () => {
43+
const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 };
44+
const state = { config: {}, folder };
45+
46+
await fastApiLaunch.buildFastAPIWithFileLaunchDebugConfiguration(instance(input), state);
47+
48+
const config = {
49+
name: DebugConfigStrings.fastapi.snippetFile.name,
50+
type: DebuggerTypeName,
51+
request: 'launch',
52+
module: 'fastapi',
53+
args: ['dev', '${file}'],
54+
jinja: true,
55+
subProcess: true,
3556
};
3657

3758
expect(state.config).to.be.deep.equal(config);

0 commit comments

Comments
 (0)