diff --git a/src/core/routerResolver.ts b/src/core/routerResolver.ts index 55107a4..6b46899 100644 --- a/src/core/routerResolver.ts +++ b/src/core/routerResolver.ts @@ -113,6 +113,25 @@ async function buildRouterGraphInternal( } } + // Factory function: if the entrypoint variable (e.g. "app" from "main:app") + // is assigned via a factory function (`app = create_app()`) rather than a direct + // FastAPI() constructor call, static analysis can't determine the type. If routes + // are decorated with @app.get(...) etc. though, we know it must be a FastAPI instance. + if ( + !appRouter && + targetVariable && + analysis.routes.some((r) => r.owner === targetVariable) + ) { + appRouter = { + variableName: targetVariable, + type: "FastAPI", + prefix: "", + tags: [], + line: 0, + column: 0, + } + } + if (!appRouter || !analysis) { return null } diff --git a/src/test/core/routerResolver.test.ts b/src/test/core/routerResolver.test.ts index 41b1a63..b37d4c0 100644 --- a/src/test/core/routerResolver.test.ts +++ b/src/test/core/routerResolver.test.ts @@ -538,5 +538,37 @@ suite("routerResolver", () => { assert.strictEqual(result.children[0].router.prefix, "/users") assert.ok(result.children[0].router.routes.length >= 2) }) + + test("infers FastAPI app when assigned via factory function (app = get_fastapi_app())", async () => { + const result = await buildRouterGraph( + fixtures.factoryFunc.mainPy, + parser, + fixtures.factoryFunc.root, + nodeFileSystem, + "app", + ) + + assert.ok( + result, + "Should find app even when assigned via factory function", + ) + assert.strictEqual(result.type, "FastAPI") + assert.strictEqual(result.variableName, "app") + assert.strictEqual(result.routes.length, 2) + const paths = result.routes.map((r) => r.path) + assert.ok(paths.includes("/1")) + assert.ok(paths.includes("/2")) + }) + + test("returns null without targetVariable when app is a factory function", async () => { + const result = await buildRouterGraph( + fixtures.factoryFunc.mainPy, + parser, + fixtures.factoryFunc.root, + nodeFileSystem, + ) + + assert.strictEqual(result, null) + }) }) }) diff --git a/src/test/fixtures/factory-func/app.py b/src/test/fixtures/factory-func/app.py new file mode 100644 index 0000000..a7f51c6 --- /dev/null +++ b/src/test/fixtures/factory-func/app.py @@ -0,0 +1,5 @@ +from fastapi import FastAPI + + +def get_fastapi_app() -> FastAPI: + return FastAPI() diff --git a/src/test/fixtures/factory-func/main.py b/src/test/fixtures/factory-func/main.py new file mode 100644 index 0000000..6266c69 --- /dev/null +++ b/src/test/fixtures/factory-func/main.py @@ -0,0 +1,13 @@ +from app import get_fastapi_app + +app = get_fastapi_app() + + +@app.get("/1") +def one(): + return "Route one" + + +@app.get("/2") +def two(): + return "Route two" diff --git a/src/test/testUtils.ts b/src/test/testUtils.ts index 6608a7d..02751c2 100644 --- a/src/test/testUtils.ts +++ b/src/test/testUtils.ts @@ -83,6 +83,10 @@ export const fixtures = { projectRoot: uri(join(fixturesPath, "monorepo", "service")), mainPy: uri(join(fixturesPath, "monorepo", "service", "myapp", "main.py")), }, + factoryFunc: { + root: uri(join(fixturesPath, "factory-func")), + mainPy: uri(join(fixturesPath, "factory-func", "main.py")), + }, } /**