New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Pydantic v2 model inspect.signature lost information #7978
Comments
import inspect
from fastapi import Query
from typing import Annotated, List
from pydantic import BaseModel
from pydantic.dataclasses import dataclass
class QueryParams(BaseModel):
q: Annotated[list[str], Query()]
@dataclass
class QueryParamsDataclass():
q: Annotated[list[str], Query()]
inspect.signature(QueryParams)
inspect.signature(QueryParamsDataclass) Replicates the issue. QueryParamsDataclass has the expected signature <Signature (q: typing.Annotated[list[str], Query(PydanticUndefined)]) -> None> so I suppose you could use that as a workaround if it's acceptable for FastApi. |
Hi @JoshYuJump, This does look like a bug. @howsunjow, thanks for the example with the comparison to the The good news is, I think this might be fixed in the latest version of pydantic. Trying with import inspect
from typing import List
from typing_extensions import Annotated
from pydantic import BaseModel
from pydantic.dataclasses import dataclass
class SomeMetadata(BaseModel):
pass
class QueryParams(BaseModel):
q: Annotated[List[str], SomeMetadata()]
@dataclass
class QueryParamsDataclass():
q: Annotated[List[str], SomeMetadata()]
print(inspect.signature(QueryParams))
#> (*, q: typing_extensions.Annotated[List[str], SomeMetadata()]) -> None
print(inspect.signature(QueryParamsDataclass))
#> (q: typing_extensions.Annotated[List[str], SomeMetadata()]) -> None We should be releasing v2.5 quite soon (hopefully today or tomorrow). I'll leave this open until we verify it's working for you with the new version @JoshYuJump. |
There's still a problem with the latest. The problem seems to be when you use FieldInfo (or a descendent class) in the annotation. import inspect
from typing import Annotated, List
from pydantic import BaseModel
from pydantic.dataclasses import dataclass
from pydantic.fields import FieldInfo
class QueryParams(BaseModel):
q: Annotated[list[str], FieldInfo(default=['abc'], annotation="List of strings")]
@dataclass
class QueryParamsDataclass():
q: Annotated[list[str], FieldInfo(default=['abc'], annotation="List of strings")]
inspect.signature(QueryParams)
#> <Signature (*, q: list[str] = ['abc']) -> None>
inspect.signature(QueryParamsDataclass)
#> <Signature (q: typing.Annotated[list[str], FieldInfo(annotation=str, required=False, default=['abc'])]) -> None> |
Thanks for the feedback. I'll look into this 👍 |
I did a bit more investigation. In pydantic 1.10.13 class QueryParams(BaseModel):
q: Annotated[list[str], FieldInfo(default=['123'], annotation="List of strings")] fails with Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "pydantic/main.py", line 197, in pydantic.main.ModelMetaclass.__new__
File "pydantic/fields.py", line 497, in pydantic.fields.ModelField.infer
File "pydantic/fields.py", line 469, in pydantic.fields.ModelField._get_field_info
ValueError: `Field` default cannot be set in `Annotated` for 'q' class QueryParams(BaseModel):
q: Annotated[list[str], FieldInfo(annotation="List of strings")] works and gives the signature <Signature (*, q: typing.Annotated[list[str], FieldInfo(default=PydanticUndefined, extra={'annotation': 'List of strings'})]) -> None> Can we assume that this should be the correct behaviour for 2.0? |
@sydney-runkle Any news on it? |
I haven't looked into this further yet. Would you like to take a shot at a fix? |
I found v2 try to split Annotated field to for example: q: Annotated[list[str], Query()] It will be split to 2 parts in FieldInfo instance: The behavior is different in v1, v1 doesn't split it. So, your team prefer to split it, but FastAPI not support it? |
@sydney-runkle The is another issue that, metadata >>> annotation
typing.Annotated[list[str], Query(None)]
>>> result = FieldInfo.from_annotation(annotation)
|
I've submitted a PR (PR #8318) to try and fix the issue. Instead of going into the weeds of what is "correct" I aimed for consistency with pydantic version 1 when it comes to signatures. |
Looking into this more now. I'm curious what your use case is for making this change. As far as I can tell, it's not essential that we include all of the information specified in an |
@sydney-runkle After more research, I found the v2's annotation is different design of v1's, some information is stored in metadata attribute in v2. My case is work with FastAPI, A lot of people have the same problem as me. |
An update on this - the fix I have proposed above breaks FastAPI schema tests, so we're working on fixing both. Going to pivot my attention to getting 2.6 out, but will return to this as a priority for 2.7! |
@JoshYuJump it would be helpful if you could explain what you are trying to achieve with FastAPI at a higher level. While we can change what the signature contains, presumably that won't help you if FastAPI doesn't handle the updated signatures correctly (which I think is what Sydney noticed — changing the way we generate the signature causes defaults to get mishandled in some cases). We can definitely work to make this better in both Pydantic and FastAPI together, but understanding what the end use case is will probably make it easier for us to make simultaneous modifications to Pydantic and FastAPI to get everything working. In particular, if you could provide a code snippet involving FastAPI and Pydantic together and explain what you are trying to achieve / how it used to work vs. how it works now, it will make it a lot easier for us to address that use case, rather than making independent changes to signature generation that may or may not actually help. |
@JoshYuJump Actually, I reviewed one of the issues you linked, and think I understand the use case: from fastapi import FastAPI, Query, Depends
from pydantic import BaseModel
from starlette.testclient import TestClient
class Request(BaseModel):
scalar_parameter: int = Query() # perfectly fine, added to parameters.
list_parameter: list[int] = Query() # this is moved to body instead of query.
app = FastAPI()
@app.get(path="/endpoint-with-query-params")
def get_resource(
scalar_parameter: int = Query(),
list_parameter: list[int] = Query()
) -> str:
return f"scalar_parameter: {scalar_parameter}, list_parameter: {list_parameter}"
@app.get(path="/endpoint-with-model-depends")
def get_resource(req: Request = Depends()) -> str:
return f"scalar_parameter: {req.scalar_parameter}, list_parameter: {req.list_parameter}"
response = TestClient(app).get("/endpoint-with-query-params?scalar_parameter=1&list_parameter=1&list_parameter=2")
print(f'{response.status_code}: {response.content.decode()}')
#> 200: "scalar_parameter: 1, list_parameter: [1, 2]"
response = TestClient(app).get("/endpoint-with-model-depends?scalar_parameter=1&list_parameter=1&list_parameter=2")
print(f'{response.status_code}: {response.content.decode()}')
#> 422: {"detail":[{"type":"missing","loc":["body"],"msg":"Field required","input":null,"url":"https://errors.pydantic.dev/2.6/v/missing"}]}
# You can see the problem here — because list[int] is inferred as being a body parameter,
# without the default assignment to Query() it is mishandled:
print(Request.__signature__)
#> (*, scalar_parameter: int, list_parameter: list[int]) -> None It seems to me you would like both of the endpoints above to return a 200 (with the same response). If it did that, would that address your issue? |
Yes, you are right. |
@sydney-runkle well, removed from the milestone? |
I'll add to 2.8! |
Initial Checks
Description
Packages versions:
pydantic 2.4.2
pydantic_core 2.10.1
pydantic-settings 2.0.3
In Pydantic v1, signature value is
While in Pydantic v2, signature value is
The lost information is
Query()
Example Code
No response
Python, Pydantic & OS Version
The text was updated successfully, but these errors were encountered: