Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@
"type": "string",
"default": "",
"scope": "resource",
"description": "Path to the main FastAPI application file (e.g., 'src/main.py'). If not set, the extension will search common locations."
"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."
},
"fastapi.codeLens.enabled": {
"type": "boolean",
Expand Down
41 changes: 25 additions & 16 deletions src/appDiscovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,25 @@ import { vscodeFileSystem } from "./vscode/vscodeFileSystem"

export type { EntryPoint }

/**
* Parses an entrypoint string in module:variable notation.
* Supports formats like "my_app.main:app" or "main".
* Returns the relative file path and optional variable name.
*/
export function parseEntrypointString(value: string): {
relativePath: string
variableName?: string
} {
const colonIndex = value.indexOf(":")
const modulePath = colonIndex === -1 ? value : value.slice(0, colonIndex)
const variableName =
colonIndex === -1 ? undefined : value.slice(colonIndex + 1)

const relativePath = `${modulePath.replace(/\./g, "/")}.py`

return { relativePath, variableName }
}

/**
* Scans for common FastAPI entry point files (main.py, __init__.py).
* Returns URI strings sorted by depth (shallower first).
Expand Down Expand Up @@ -64,18 +83,8 @@ async function parsePyprojectForEntryPoint(
return null
}

// Parse "my_app.main:app" or "api.py:app" format (variable name after : is optional)
const colonIndex = entrypointValue.indexOf(":")
const modulePath =
colonIndex === -1 ? entrypointValue : entrypointValue.slice(0, colonIndex)
const variableName =
colonIndex === -1 ? undefined : entrypointValue.slice(colonIndex + 1)

// Handle both module format (api.module) and file format (api.py)
const relativePath =
modulePath.endsWith(".py") && !modulePath.includes("/")
? modulePath // Simple file path: api.py -> api.py
: `${modulePath.replace(/\./g, "/")}.py` // Module path: my_app.main -> my_app/main.py
const { relativePath, variableName } =
parseEntrypointString(entrypointValue)
const fullUri = vscode.Uri.joinPath(folderUri, relativePath)

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

// If user specified an entry point in settings, use that
if (customEntryPoint) {
const entryUri = customEntryPoint.startsWith("/")
? vscode.Uri.file(customEntryPoint)
: vscode.Uri.joinPath(folder.uri, customEntryPoint)
const { relativePath, variableName } =
parseEntrypointString(customEntryPoint)
const entryUri = vscode.Uri.joinPath(folder.uri, relativePath)

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

log(`Using custom entry point: ${customEntryPoint}`)
candidates = [{ filePath: entryUri.toString() }]
candidates = [{ filePath: entryUri.toString(), variableName }]
detectionMethod = "config"
} else {
// Otherwise, check pyproject.toml or auto-detect
Expand Down
34 changes: 34 additions & 0 deletions src/test/appDiscovery.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import * as assert from "node:assert"
import { parseEntrypointString } from "../appDiscovery"

suite("parseEntrypointString", () => {
test("module notation with variable: my_app.main:app", () => {
const result = parseEntrypointString("my_app.main:app")
assert.strictEqual(result.relativePath, "my_app/main.py")
assert.strictEqual(result.variableName, "app")
})

test("module notation without variable: my_app.main", () => {
const result = parseEntrypointString("my_app.main")
assert.strictEqual(result.relativePath, "my_app/main.py")
assert.strictEqual(result.variableName, undefined)
})

test("deeply nested module: a.b.c.main:application", () => {
const result = parseEntrypointString("a.b.c.main:application")
assert.strictEqual(result.relativePath, "a/b/c/main.py")
assert.strictEqual(result.variableName, "application")
})

test("single module name: app", () => {
const result = parseEntrypointString("app")
assert.strictEqual(result.relativePath, "app.py")
assert.strictEqual(result.variableName, undefined)
})

test("single module with variable: app:my_app", () => {
const result = parseEntrypointString("app:my_app")
assert.strictEqual(result.relativePath, "app.py")
assert.strictEqual(result.variableName, "my_app")
})
})
Loading