Skip to content

Commit cd2c2ac

Browse files
Merge branch 'main' into endpoint_to_path
2 parents 8f8b9ff + 0d320ad commit cd2c2ac

12 files changed

Lines changed: 188 additions & 30 deletions

File tree

bun.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
},
1515
"homepage": "https://github.com/fastapi/fastapi-vscode#readme",
1616
"engines": {
17-
"vscode": "^1.85.0"
17+
"vscode": "^1.95.0"
1818
},
1919
"main": "./dist/extension.js",
2020
"browser": "./dist/web/extension.js",
@@ -303,7 +303,7 @@
303303
"type": "string",
304304
"default": "",
305305
"scope": "resource",
306-
"description": "Path to the main FastAPI application file (e.g., 'src/main.py'). If not set, the extension will search common locations."
306+
"description": "Entrypoint for the main FastAPI application in module notation (e.g., 'my_app.main:app'). If not set, the extension will search pyproject.toml and common locations."
307307
},
308308
"fastapi.codeLens.enabled": {
309309
"type": "boolean",
@@ -344,7 +344,7 @@
344344
"@types/bun": "latest",
345345
"@types/mocha": "^10.0.10",
346346
"@types/sinon": "^21.0.0",
347-
"@types/vscode": "^1.85.0",
347+
"@types/vscode": "^1.95.0",
348348
"@vscode/test-cli": "^0.0.12",
349349
"@vscode/test-electron": "^2.5.2",
350350
"@vscode/vsce": "^3.7.1",

scripts/test-coverage.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
#!/bin/bash
22
set -e
33

4+
# Clean dist to avoid stale files from previous bundled builds
5+
rm -rf dist
6+
47
# Build without bundling (required for per-file coverage)
58
bun run esbuild.js --no-bundle
69

src/appDiscovery.ts

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,25 @@ import { vscodeFileSystem } from "./vscode/vscodeFileSystem"
1818

1919
export type { EntryPoint }
2020

21+
/**
22+
* Parses an entrypoint string in module:variable notation.
23+
* Supports formats like "my_app.main:app" or "main".
24+
* Returns the relative file path and optional variable name.
25+
*/
26+
export function parseEntrypointString(value: string): {
27+
relativePath: string
28+
variableName?: string
29+
} {
30+
const colonIndex = value.indexOf(":")
31+
const modulePath = colonIndex === -1 ? value : value.slice(0, colonIndex)
32+
const variableName =
33+
colonIndex === -1 ? undefined : value.slice(colonIndex + 1)
34+
35+
const relativePath = `${modulePath.replace(/\./g, "/")}.py`
36+
37+
return { relativePath, variableName }
38+
}
39+
2140
/**
2241
* Scans for common FastAPI entry point files (main.py, __init__.py).
2342
* Returns URI strings sorted by depth (shallower first).
@@ -64,18 +83,8 @@ async function parsePyprojectForEntryPoint(
6483
return null
6584
}
6685

67-
// Parse "my_app.main:app" or "api.py:app" format (variable name after : is optional)
68-
const colonIndex = entrypointValue.indexOf(":")
69-
const modulePath =
70-
colonIndex === -1 ? entrypointValue : entrypointValue.slice(0, colonIndex)
71-
const variableName =
72-
colonIndex === -1 ? undefined : entrypointValue.slice(colonIndex + 1)
73-
74-
// Handle both module format (api.module) and file format (api.py)
75-
const relativePath =
76-
modulePath.endsWith(".py") && !modulePath.includes("/")
77-
? modulePath // Simple file path: api.py -> api.py
78-
: `${modulePath.replace(/\./g, "/")}.py` // Module path: my_app.main -> my_app/main.py
86+
const { relativePath, variableName } =
87+
parseEntrypointString(entrypointValue)
7988
const fullUri = vscode.Uri.joinPath(folderUri, relativePath)
8089

8190
return (await vscodeFileSystem.exists(fullUri.toString()))
@@ -117,9 +126,9 @@ export async function discoverFastAPIApps(
117126

118127
// If user specified an entry point in settings, use that
119128
if (customEntryPoint) {
120-
const entryUri = customEntryPoint.startsWith("/")
121-
? vscode.Uri.file(customEntryPoint)
122-
: vscode.Uri.joinPath(folder.uri, customEntryPoint)
129+
const { relativePath, variableName } =
130+
parseEntrypointString(customEntryPoint)
131+
const entryUri = vscode.Uri.joinPath(folder.uri, relativePath)
123132

124133
if (!(await vscodeFileSystem.exists(entryUri.toString()))) {
125134
log(`Custom entry point not found: ${customEntryPoint}`)
@@ -130,7 +139,7 @@ export async function discoverFastAPIApps(
130139
}
131140

132141
log(`Using custom entry point: ${customEntryPoint}`)
133-
candidates = [{ filePath: entryUri.toString() }]
142+
candidates = [{ filePath: entryUri.toString(), variableName }]
134143
detectionMethod = "config"
135144
} else {
136145
// Otherwise, check pyproject.toml or auto-detect

src/cloud/auth.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,11 @@ export class CloudAuthenticationProvider
247247

248248
const sessions = await this.getSessions()
249249
const session = sessions[0]
250+
251+
window.showInformationMessage(
252+
`Signed in to FastAPI Cloud as ${session.account.label}`,
253+
)
254+
250255
this._onDidChangeSessions.fire({
251256
added: [session],
252257
removed: [],

src/cloud/commands/logs.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,12 +134,12 @@ export function getWebviewHtml(
134134
<div class="filter-hint">Filters apply to displayed logs</div>
135135
</div>
136136
</div>
137-
<button id="stream-btn" title="Start streaming"><span id="stream-label">Stream</span></button>
137+
<button id="stream-btn" title="Start streaming"><span id="stream-label">Start</span></button>
138138
<span id="app-label"></span>
139139
<div class="spacer"></div>
140140
<button class="icon-btn" id="clear-btn" title="Clear logs"><svg width="12" height="12" viewBox="0 0 16 16"><path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/><path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4L4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/></svg>Clear</button>
141141
</div>
142-
<div id="logs"><span class="status">Click Stream to fetch logs.</span></div>
142+
<div id="logs"><span class="status">Click "Start" to stream logs.</span></div>
143143
<script src="${scriptUri}"></script>
144144
</body>
145145
</html>`

src/cloud/ui/panel/webview.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,15 @@ streamBtn.addEventListener("click", () => {
3737
}
3838
})
3939

40+
sinceFilter.addEventListener("change", () => {
41+
if (isStreaming) {
42+
vscode.postMessage({
43+
type: "startStream",
44+
since: sinceFilter.value,
45+
})
46+
}
47+
})
48+
4049
filterBtn.addEventListener("click", (e) => {
4150
e.stopPropagation()
4251
filterPopup.classList.toggle("open")
@@ -116,7 +125,6 @@ function setStreamingState(streaming: boolean, appLabel?: string): void {
116125
label.textContent = "Stream"
117126
streamBtn.title = "Start streaming"
118127
}
119-
sinceFilter.disabled = streaming
120128
appLabelEl.textContent =
121129
streaming && appLabel ? `Streaming logs for ${appLabel}...` : ""
122130
}

src/extension.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -275,12 +275,17 @@ export async function activate(context: vscode.ExtensionContext) {
275275
)
276276
}
277277

278-
// Watch for cloud.enabled setting changes
279278
context.subscriptions.push(
280279
vscode.workspace.onDidChangeConfiguration(async (e) => {
281-
if (e.affectsConfiguration("fastapi.cloud.enabled")) {
280+
const requiresReload =
281+
e.affectsConfiguration("fastapi.cloud.enabled") ||
282+
e.affectsConfiguration("fastapi.codeLens.enabled") ||
283+
e.affectsConfiguration("fastapi.entryPoint") ||
284+
e.affectsConfiguration("fastapi.telemetry.enabled")
285+
286+
if (requiresReload) {
282287
const action = await vscode.window.showWarningMessage(
283-
"FastAPI Cloud setting changed. Reload the window to apply changes.",
288+
"FastAPI setting changed. Reload the window to apply changes.",
284289
"Reload Window",
285290
)
286291
if (action === "Reload Window") {
@@ -435,6 +440,7 @@ function registerCommands(
435440

436441
const selected = await vscode.window.showQuickPick(items, {
437442
placeHolder: "Search FastAPI path operations...",
443+
matchOnDescription: true,
438444
})
439445
trackSearchExecuted(items.length, selected !== undefined)
440446
if (selected) {

src/test/appDiscovery.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import * as assert from "node:assert"
2+
import { parseEntrypointString } from "../appDiscovery"
3+
4+
suite("parseEntrypointString", () => {
5+
test("module notation with variable: my_app.main:app", () => {
6+
const result = parseEntrypointString("my_app.main:app")
7+
assert.strictEqual(result.relativePath, "my_app/main.py")
8+
assert.strictEqual(result.variableName, "app")
9+
})
10+
11+
test("module notation without variable: my_app.main", () => {
12+
const result = parseEntrypointString("my_app.main")
13+
assert.strictEqual(result.relativePath, "my_app/main.py")
14+
assert.strictEqual(result.variableName, undefined)
15+
})
16+
17+
test("deeply nested module: a.b.c.main:application", () => {
18+
const result = parseEntrypointString("a.b.c.main:application")
19+
assert.strictEqual(result.relativePath, "a/b/c/main.py")
20+
assert.strictEqual(result.variableName, "application")
21+
})
22+
23+
test("single module name: app", () => {
24+
const result = parseEntrypointString("app")
25+
assert.strictEqual(result.relativePath, "app.py")
26+
assert.strictEqual(result.variableName, undefined)
27+
})
28+
29+
test("single module with variable: app:my_app", () => {
30+
const result = parseEntrypointString("app:my_app")
31+
assert.strictEqual(result.relativePath, "app.py")
32+
assert.strictEqual(result.variableName, "my_app")
33+
})
34+
})

src/test/cloud/auth.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,57 @@ suite("cloud/auth", () => {
387387
await provider.dispose()
388388
})
389389

390+
test("shows notification after device code sign-in", async () => {
391+
fsStub.fake.readFile.rejects(new Error("File not found"))
392+
393+
const token = validToken()
394+
const fetchStub = sinon.stub(globalThis, "fetch")
395+
fetchStub.onFirstCall().resolves(
396+
mockResponse({
397+
device_code: "dev123",
398+
user_code: "USER-CODE",
399+
verification_uri: "https://auth.example.com/device",
400+
verification_uri_complete:
401+
"https://auth.example.com/device?code=USER-CODE",
402+
interval: 1,
403+
}),
404+
)
405+
fetchStub.onSecondCall().resolves(mockResponse({ access_token: token }))
406+
fetchStub
407+
.onThirdCall()
408+
.resolves(
409+
mockResponse({ email: "new@example.com", full_name: "New" }),
410+
)
411+
412+
sinon.stub(vscode.env, "openExternal")
413+
sinon
414+
.stub(vscode.window, "withProgress")
415+
.callsFake(async (_opts, task) => {
416+
const cancellationToken = {
417+
isCancellationRequested: false,
418+
onCancellationRequested: sinon.stub(),
419+
}
420+
return task({ report: sinon.stub() }, cancellationToken as any)
421+
})
422+
const infoStub = sinon.stub(vscode.window, "showInformationMessage")
423+
424+
fsStub.fake.createDirectory.resolves()
425+
fsStub.fake.writeFile.callsFake(async () => {
426+
fsStub.fake.readFile.resolves(
427+
Buffer.from(JSON.stringify({ access_token: token })),
428+
)
429+
})
430+
431+
const { provider } = createProvider()
432+
await provider.createSession()
433+
434+
assert.ok(
435+
infoStub.calledWith("Signed in to FastAPI Cloud as new@example.com"),
436+
)
437+
438+
await provider.dispose()
439+
})
440+
390441
test("wraps network error with friendly message", async () => {
391442
fsStub.fake.readFile.rejects(new Error("File not found"))
392443

0 commit comments

Comments
 (0)