FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

Use `fastapi run` for FastAPI debug configs, with file and project variants by savannahostrowski · Pull Request #1048 · microsoft/vscode-python-debugger · GitHub

Use fastapi run for FastAPI debug configs, with file and project variants - #1048

Open
Savannah Ostrowski (savannahostrowski) wants to merge 11 commits into
microsoft:mainfrom
savannahostrowski:fastapi-updates
Open

Use fastapi run for FastAPI debug configs, with file and project variants#1048
Savannah Ostrowski (savannahostrowski) wants to merge 11 commits into
microsoft:mainfrom
savannahostrowski:fastapi-updates

Conversation

Copy link
Copy Markdown

A draft for #1045 - opening this to make the proposed approach a bit more concrete. Happy to adjust based on the open discussion before marking it review ready (there are some open questions to align on in the issue).

This updates the FastAPI debug configuration (both the launch.json snippet and the dynamic provider) to use the fastapi CLI instead of invoking uvicorn directly:

  • Debug FastAPI with current file - runs fastapi run ${file} against the currently active file. This is optimal for single-file apps or quickly debugging a specific entry point.

  • Debug FastAPI - runs plain fastapi run and lets the CLI discover the app via pyproject.toml or default file locations.

This replaces the previous uvicorn main:app --reload config and its hand-rolled main.py detection.

I have run the tests and manually tested as well.

Copy link
Copy Markdown
Contributor

/azp run

Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree [company="FastAPI Labs"]

Copy link
Copy Markdown
Azure Pipelines could not run because the pipeline triggers exclude this branch/path.

Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree company="FastAPI Labs"

Rich Chiodo (rchiodo) added the debt Code quality issues label Jun 1, 2026

Savannah Ostrowski (savannahostrowski) commented Jun 1, 2026
edited
Loading

Copy link
Copy Markdown
Author

So just closing the loop here and reconciling threads from #1045, since the PR only changes what the snippet flow writes when a user newly invokes "Add Configuration" and what the dynamic provider suggests in-memory for users without a launch.json, anyone with module: uvicorn already in their launch.json keeps that exact config and continues to work.

I've also added tiered auto-detection so monorepo/nested layouts don't break out of the box:

Auto-detection (applied in both flows):

  • 1 match at workspace root → plain fastapi run
  • 1 match in a subdirectory → fastapi run (auto-passed)
  • 2+ matches → snippet prompts the user (pre-filled with the first detection); dynamic provider falls back to plain fastapi run

The snippet flow also prompts on 0 matches (with no pre-fill). The dynamic provider doesn't surface FastAPI configs at all if 0 are detected.

Savannah Ostrowski (savannahostrowski) marked this pull request as ready for review June 1, 2026 18:52
export async function buildFastAPIWithFileLaunchDebugConfiguration(
_input: MultiStepInput<DebugConfigurationState>,
state: DebugConfigurationState,
): Promise<void> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Copilot generated:
The Architect notes the ['run', '${file}'] config literal now lives in two places (here and dynamicdebugConfigurationService.ts), and the ['run'] default is encoded both in tryResolveFastApiArgs and as ?? ['run'] in the dynamic caller. Hoist to a shared constant / small helper to prevent drift. Low priority.

[verified]

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

I tried this but extracting ['run', '${file}'] into a readonly string[] constant required spreading at every call site ([...FASTAPI_RUN_FILE_ARGS]), which was longer than the inline literal.

return fastApiPaths;
}

export function tryResolveFastApiArgs(folder: WorkspaceFolder, paths: readonly Uri[]): string[] | undefined {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Copilot generated:
Unresolved from prior review (Medium): path.relative emits OS-native separators, so a single subdir match persists "args": ["run", "backend\\app\\main.py"] on Windows. A launch.json shared to macOS/Linux then runs fastapi run backend\app\main.py, which treats backslashes literally and fails despite correct detection. The Skeptic notes the central fix actually broadened exposure here (root single-matches previously emitted separator-free ['run']). Normalize to POSIX in the persisted arg (e.g. relative.split(path.sep).join('/')) and confirm fastapi run accepts forward slashes on Windows.

[verified]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Savannah Ostrowski (@savannahostrowski) what did you think of this idea? Does fastapi work with forward slashes on Windows?

const appPyPath = path.join(folder.uri.fsPath, 'main.py');
pathExistsStub.withArgs(appPyPath).resolves(false);
const file = await fastApiLaunch.getApplicationPath(folder);
const state = { config: {}, folder };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Copilot generated:
The Skeptic notes the nested-match expectations use path.join('backend','app','main.py'), which produces the resolver's own OS-specific output on every platform — the separator assertion is tautological and will never catch the portability bug above. Once the persisted arg is normalized to POSIX, assert the literal 'backend/app/main.py' instead.

[verified]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Of course that other change would affect this test if you modified it to use forward slashes.

Copy link
Copy Markdown
Author

Rich Chiodo (@rchiodo) I addressed most of the comments you've left. The two outstanding are tied to the path-separator decision I called out in this thread.

Happy to revisit if you'd prefer to normalize across all three providers, but didn't want to diverge from the existing pattern unilaterally.

Let me know if there's anything else you'd like changed!

Copy link
Copy Markdown
Contributor

Savannah Ostrowski (@savannahostrowski) can you merge/rebase on main? The tests fail because of an unrelated issue which I just submitted a fix for on the main branch.

Rich Chiodo (rchiodo) commented Jul 6, 2026
edited
Loading

Copy link
Copy Markdown
Contributor

🔒 Automated review in progress — Rich Chiodo (@rchiodo) is auto-reviewing this PR.

module: 'uvicorn',
args: ['main:app', '--reload'],
module: 'fastapi',
args,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

module: 'fastapi' runs python -m fastapi run …, which requires fastapi-cli (fastapi[standard]) or FastAPI ≥ 0.111. The previous module: 'uvicorn' worked with a bare uvicorn install, so environments that debugged fine before can now fail immediately with No module named fastapi.__main__. Consider detecting CLI availability or surfacing an actionable message, and document the minimum FastAPI/fastapi-cli requirement.

if (paths.length !== 1) {
return undefined;
}
const relative = path.relative(folder.uri.fsPath, paths[0].fsPath);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

path.relative yields OS-native separators (e.g. backend\app\main.py on Windows) that get baked into args and persisted to launch.json. A committed launch.json should be portable; consider normalizing to POSIX separators (fastapi run accepts / on Windows) so a shared config works cross-platform.

}
const relative = path.relative(folder.uri.fsPath, paths[0].fsPath);
return ['run', relative];
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Multi-root workspace edge case: getFastApiPaths uses workspace-wide workspace.findFiles, so tryResolveFastApiArgs(folder, paths) can compute path.relative(folder, fileInAnotherFolder) → ..\otherFolder\main.py, which is then passed straight to fastapi run and points outside the folder. Consider filtering detected paths to the target folder, or treating cross-folder matches as "not a single match" and falling back to plain run.

request: 'launch',
module: 'fastapi',
args: ['run', '${file}'],
jinja: true,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

The "FastAPI File" (${file}) variant is only pushed inside if (fastApiPaths?.length), so a user whose app doesn't match the app\s*=\s*FastAPI\( regex (e.g. application = FastAPI(), a factory pattern, or app in __init__.py) gets neither config — even though fastapi run ${file} would work on their open file. The ${file} variant doesn't depend on project detection; consider offering it independently of the detection gate.

}
const relative = path.relative(folder.uri.fsPath, paths[0].fsPath);
return ['run', relative];
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

tryResolveFastApiArgs returns undefined for BOTH "0 matches" and ">1 matches", and the two callers resolve that ambiguity differently (dynamic provider → ?? ['run']; interactive builder → prompt). This is intentional and tested, but undocumented. Add a one-line comment stating it returns args only for an unambiguous single match and that callers decide how to handle 0-or-many, so a future maintainer doesn't "fix" it and break one path.

const state = { config: {}, folder };
getFastApiPathsStub.resolves([Uri.parse(path.join('one', 'two', 'backend', 'app', 'main.py'))]);

await fastApiLaunch.buildFastAPILaunchDebugConfiguration(instance(input), state);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

The prefill computed at fastapiLaunch.ts (path.relative(workspaceRoot, fastApiPaths[0].fsPath)) is never asserted — the multi-match test stubs showInputBox but doesn't check the value (prefill) argument, so a wrong prefill would pass CI. Also untested: the telemetry fields autoDetectedFastAPIMainPyPath/manuallyEnteredAValue and the state.folder === undefined branch. Consider adding assertions for the prefill value and telemetry payload.

Copy link
Copy Markdown
Contributor

Approach looks reasonable. The main things worth aligning on before marking review-ready: the switch from uvicorn to the fastapi CLI raises a runtime dependency (fastapi-cli / FastAPI ≥ 0.111), the detected path is baked into launch.json with OS-native separators, and the workspace-wide path detection can produce cross-folder relative paths in multi-root workspaces. None are blocking, but they affect portability and edge-case correctness.

Rich Chiodo (rchiodo) left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Approved via Review Center.

This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

debt Code quality issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants


Back | FazBrowse Home | New Git URL