Description
(See https://clojurians.slack.com/archives/C071RFV2Z1D/p1734990790884169 for context)
Hi,
There appear to exist Python libraries that provide function decorators which expect the function they decorate to have specific parameter names.
One such library is FastAPI. Below is an example taken from the FastAPI
documentation:
It creates two endpoints:
- The
/
endpoint will return the dictionary{"message": "World"}
dict (.e.g. at http://localhost/). - The
/items/{itemd_id}
endpoint will return a dictionary with theitem_id
, e.g. athttp://localhost/items/abcd
it will return{"item_id": "abcd"}
@app.get("/")
def read_root():
return {"message": "World"}
@app.get("/items/{item_id}")
def read_item(item_id: int, q: Union[str, None] = None):
return {"item_id": item_id, "q": q}
In this example, the {item_id}
in the decorator corresponds to the item_id
parameter in the function. This allows FastAPI to bind the path parameter to the function parameter automatically.
However, Basilisp appears to modify function parameter names by appending a suffix through the basilisp.lang.util.genname
function. As a result, a hypothetical parameter like abc
would become abc_nnn
when compiled down to Python. This makes it impossible to align the parameter names with those expected by decorators from libraries like FastAPI.
For instance, if you try to use the following namespace with a FastAPI decorator that expects a name
parameter:
(ns basilex-fastapi.ex
(:import [fastapi :as f]
[uvicorn :as uv]))
(defonce app (f/FastAPI))
(defn root {:async true
:decorators [(.get app "/")]}
[]
{"message" "Hi there"})
(defn hello {:async true
:decorators [(.get app "/hello/{name}")]}
[name]
{"name" name})
(comment
(def server (future (uv/run app ** :host "127.0.0.1" :port 8000))))
The resulting Python code will fail to bind the path parameter name
to the function parameter because Basilisp renames the name
parameter to name_nnn
when creating the function. Therefore, FastAPI will not find a name
parameter to bind the value passed in the URL (e.g., http://localhost:8000/hello/xyz
).
This issue arises due to Basilisp’s use of genname
, which changes the parameter names. The
basilisp/src/basilisp/lang/compiler/generator.py
Line 1638 in 5525a69
genname
is used to alter parameter names:
def __fn_args_to_py_ast(
ctx: GeneratorContext, params: Iterable[Binding], body: Do
) -> tuple[list[ast.arg], Optional[ast.arg], list[ast.stmt], Iterable[PyASTNode]]:
"""Generate a list of Python AST nodes from function method parameters."""
fn_args, varg = [], None
fn_body_ast: list[ast.stmt] = []
fn_def_deps: list[PyASTNode] = []
for binding in params:
assert binding.init is None, ":fn nodes cannot have binding :inits"
assert varg is None, "Must have at most one variadic arg"
arg_name = genname(munge(binding.name))
#...
It's unclear why genname
is essential in this context. If I remove genname
(but keep the mungei
ng), the isolated example works as expected. However, removing genname
causes Basilisp to fail to bootstrap when recompiling the codebase from scratch.
Is there a way to bypass the function parameter name uniquefication process in some cases, particularly when it doesn't appear to be required?
One possible approach would be to introduce metadata to indicate that a parameter name should be preserved. For example, we could use a metadata marker like :preserve-parameter-name
to advise the Basilisp generator not to modify the parameter name during compilation.
(defn hello {:async true
:decorators [(.get app "/hello/{name}")]}
[^:preserve-parameter-name name]
{"name" name})
Thanks