diff --git a/package.json b/package.json index 4c9c0fd..7a93052 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/appDiscovery.ts b/src/appDiscovery.ts index 8ab84d6..972f879 100644 --- a/src/appDiscovery.ts +++ b/src/appDiscovery.ts @@ -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). @@ -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())) @@ -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}`) @@ -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 diff --git a/src/test/appDiscovery.test.ts b/src/test/appDiscovery.test.ts new file mode 100644 index 0000000..887a1be --- /dev/null +++ b/src/test/appDiscovery.test.ts @@ -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") + }) +})