đź‘‹ âť“Proposal - marking parameters with annotations
vitalik opened this issue · 12 comments
@adriangb in xpresso framework created what I think a nicer approach for marking api function arguments. I think it should be the default way in django-ninja as well
basically this:
@api.post("/some")
def some(request, filters:Filters = Query(...), payload: SomeSchema = Form(...)):
...
can turn into this
@api.post("/some")
def some(request, filters: Query[Filters], payload: Form[SomeSchema]):
...
Optional params will turn from
foo: int | None = Query(None)
to
foo: Query[int | None] = None
passing OpenApi parameters:
foo: Query[int, OpenAPI(title="A Foo", example=42)]
Pros
- somewhat more intuitive definitions
- no weird "..." for required params (which I see lot of people are very surprised at first usage :) )
Cons
- most likely not backward compatible
- can become backward compatible with different naming like - FromQuery, FromBody, FromPath
Please share your thoughts
cc: @SmileyChris @OtherBarry @baseplate-admin @stephenrauch @jkeyes
First impression: I like the idea, it feels cleaner and more pythonic.
Some general notes:
- This has to still work nicely with type checkers / IDEs, ideally without having to add an extra package or mypy plugin or whatever. One of the best parts of django ninja is that type checkers 'just work'. I don't think the benefits of this system would outweigh the type checking loss if it doesn't work smoothly.
- I think it'd be ideal to keep this backwards compatible. Ideally this would be with the same class names, but I'd lean on the side of something like
FromQuery
orQueryVar
over losing backwards compatibilty. - Personally not a fan of the PEP593 style of adding openapi args, but I like the idea of being able to have different classses that extend the type, rather than having heaps of kwargs on one class.
Basically, its a good idea, I just don't want to lose the current way of doing things in order to gain it.
This will work with IDEs. In fact, it will be better in a lot of ways, especially w.r.t. default values.
Backwards compatibility with the same name is unfortunately not possible. You can however have it if you use different names. I’d recommend keeping foo: int = Query(**kwargs)
and making FromQuery = Annotated[T, Query()]
foo: Query[int, OpenAPI(title="A Foo", example=42)]
I don’t think that will work. You’ll need to do one of:
foo: Annotated[int, Query(), OpenAPI(…)]
- foo: FromQuery[Annotated[int, OpenAPI(…)]]`
IMO both are equally ugly and verbose, but they result in the same thing (internally you don’t have to handle them separately or anything).
So my first impression on this : It's a new way of approaching a rather complex problem.
Now my 2 cents on this topic is :
- I always thought of
django-ninja
as a replacement offastapi
. So those who wants the functionality, security and more specifically the admin panel and orm ofdjango
should switch over todjango-ninja
. Keeping the sameAPI
asfastapi
meant the transition is smoother. It also has the bonus point that almost all patterns written forfastapi
applies todjango
( excluding dependency injection ) - Without
codemod
. This breaking change is not feasible for large codebases. My own project, i have over 7,000 LOC of python codes that would be very hard to migrate if we choose to drop old API.
no weird "..." for required params (which I see lot of people are very surprised at first usage :) )
Maybe we can follow current fastapi approach? ( eg : adding a title? )
@api.post("/some")
def some(request, filters:Filters = Query(...), payload: SomeSchema = Form(title="some special schema which does awesome things")):
...
Note that fastapi changed quite a lot in the recent times. For example old versions of fastapi ( to my knowledge ) used to have Query
function. Now that's no longer the case
Overall the direction i want ninja to head in is to market itself as the fastapi
of django
eco-system ( but it could be a me problem here )..
Thanks for reading. It's great to see the development resuming. You have created a really awesome framework @vitalik
FastAPI actually supports and recommends this approach now: https://github.com/tiangolo/fastapi/releases/tag/0.95.0.
also, this doesn’t have to be a breaking change
FastAPI actually supports and recommends this approach now: https://github.com/tiangolo/fastapi/releases/tag/0.95.0.
Fair enough. As long as we have a step by step migration for django-ninja
, i wont complain.
also, this doesn’t have to be a breaking change
But follow a deprecation like django
handles it ?
I checked Annotated's Test Code because the documentation hasn't been updated yet!
It's a little different from fastapi's, fastapi requires that parameters must set default values, e.g. Query Annotated
django-ninja seems to set them to None by default, but some additional test code is needed!
My point:
Regular typing and PEP593 is sufficient, it is also a standard, for this proposal, in the source code, Query...
These are exported as functions and may not be appropriate for direct use in type annotations, it is better to use Type[class]
constructs as type hints
not sure which part you see different, but this should work iudentical in django-ninja and fastapi:
q: Annotated[str, Query()],
# equals to
q: str = Query(...)
OR
q: Annotated[str, Query()] = "some-defatul",
# equals to
q: str = Query("some-defatul")
but the Topic here to make it shorter:
q: Query[str] = "some-default"
with support of autocompletion and other stutff
I do like the new syntax a bit more, but I also don't want to change my annotations for every endpoint at once. Some backwards compatible change would be best (although I'd prefer the same naming schema, FromQuery
is weird – on the other hand, you can always use import as
).
Migration guide for django-ninja==1.0.0
would be greatly appreciated. Migration tool would be a great addition, but I consider the guide more important. pydantic
itself has a good migration guide for v2
, that could be referenced from the django-ninja
migration guide.
Well the plan is actually to keep all these 3 in working (backwards compatible)
q: Query[str] = "some-defatul"
# is equal to
q: Annotated[str, Query()] = "some-defatul",
# and equal to
q: str = Query("some-defatul")
# and all will work in the same codebase
it just the default documentation will suggest to use the new syntax by default