Skip to content

Commit f2749c0

Browse files
🐛 Add support to extractors for keyword args in definitions (#95)
1 parent 4b8645e commit f2749c0

2 files changed

Lines changed: 68 additions & 5 deletions

File tree

src/core/extractors.ts

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -178,10 +178,11 @@ export function decoratorExtractor(node: Node): RouteInfo | null {
178178
return null
179179
}
180180

181-
// Skip comment nodes to find the actual first argument
182-
const pathArgNode = argumentsNode.namedChildren.find(
181+
// Find path: first positional arg, or "path" keyword argument
182+
const nonCommentArgs = argumentsNode.namedChildren.filter(
183183
(child) => child.type !== "comment",
184184
)
185+
const pathArgNode = resolveArgNode(nonCommentArgs, 0, "path")
185186
const path = pathArgNode ? extractPathFromNode(pathArgNode) : ""
186187

187188
// For api_route, extract methods from keyword argument
@@ -371,6 +372,33 @@ export function importExtractor(node: Node): ImportInfo | null {
371372
return { modulePath, names, namedImports, isRelative, relativeDots }
372373
}
373374

375+
/**
376+
* Resolves a function argument value node by positional index or keyword name.
377+
*
378+
* Examples:
379+
* app.get("/users", response_model=List[User]) → position 0 = string node "/users"
380+
* app.get(path="/users", response_model=List[User]) → keyword "path" = string node "/users"
381+
*/
382+
function resolveArgNode(
383+
args: Node[],
384+
position: number,
385+
keywordName: string,
386+
): Node | undefined {
387+
const positional = args.filter((a) => a.type !== "keyword_argument")
388+
if (positional[position]) {
389+
return positional[position]
390+
}
391+
return (
392+
args
393+
.find(
394+
(a) =>
395+
a.type === "keyword_argument" &&
396+
a.childForFieldName("name")?.text === keywordName,
397+
)
398+
?.childForFieldName("value") ?? undefined
399+
)
400+
}
401+
374402
/** Extracts method call info: object.method(args) */
375403
function extractMethodCall(
376404
node: Node,
@@ -420,9 +448,12 @@ export function includeRouterExtractor(node: Node): IncludeRouterInfo | null {
420448
}
421449
}
422450

451+
// Find router: first positional arg, or "router" keyword argument
452+
const routerNode = resolveArgNode(call.args, 0, "router")
453+
423454
return {
424455
owner: call.object,
425-
router: call.args[0]?.text ?? "",
456+
router: routerNode?.text ?? "",
426457
prefix,
427458
tags,
428459
}
@@ -435,9 +466,13 @@ export function mountExtractor(node: Node): MountInfo | null {
435466
return null
436467
}
437468

469+
// Find path and app: positional or keyword argument style
470+
const pathNode = resolveArgNode(call.args, 0, "path")
471+
const appNode = resolveArgNode(call.args, 1, "app")
472+
438473
return {
439474
owner: call.object,
440-
path: extractPathFromNode(call.args[0]),
441-
app: call.args[1].text,
475+
path: pathNode ? extractPathFromNode(pathNode) : "",
476+
app: appNode?.text ?? "",
442477
}
443478
}

src/test/core/extractors.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,23 @@ def handle_items():
223223
assert.strictEqual(result.method, "GET")
224224
})
225225

226+
test("extracts route with 'path' keyword argument", () => {
227+
const code = `
228+
@app.get(path="/users/{user_id}", include_in_schema=False)
229+
def get_user(user_id: int):
230+
pass
231+
`
232+
const tree = parse(code)
233+
const decoratedDefs = findNodesByType(
234+
tree.rootNode,
235+
"decorated_definition",
236+
)
237+
const result = decoratorExtractor(decoratedDefs[0])
238+
239+
assert.ok(result)
240+
assert.strictEqual(result.path, "/users/{user_id}")
241+
})
242+
226243
test("returns null for non-decorated definition", () => {
227244
const code = `
228245
def regular_function():
@@ -576,6 +593,17 @@ def list_users():
576593
assert.deepStrictEqual(result.tags, ["users", "admin"])
577594
})
578595

596+
test("extracts include_router with 'router' keyword argument", () => {
597+
const code = `app.include_router(router=users_router, prefix="/api")`
598+
const tree = parse(code)
599+
const calls = findNodesByType(tree.rootNode, "call")
600+
const result = includeRouterExtractor(calls[0])
601+
602+
assert.ok(result)
603+
assert.strictEqual(result.router, "users_router")
604+
assert.strictEqual(result.prefix, "/api")
605+
})
606+
579607
test("returns null for non-include_router call", () => {
580608
const code = "app.some_method(arg)"
581609
const tree = parse(code)

0 commit comments

Comments
 (0)