-
-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Raise a better user error when failing to evaluate a forward reference #10030
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
Conversation
CodSpeed Performance ReportMerging #10030 will not alter performanceComparing Summary
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice work - looks solid overall, but I had one question about preserving the logical flow in general - are we missing a check for is_backport_fixable?
pydantic/_internal/_typing_extra.py
Outdated
| ) from e | ||
|
|
||
| return eval_type_backport(value, globalns, localns, try_default=False) | ||
| if isinstance(value, typing.ForwardRef): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like we haven't exactly preserved the logical flow here - don't we need a is_backport_fixable_error(e)) check in the case where we jump straight to raise e?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The check is present on line 269, however the TypeError message seems to have changed in Python 3.11:
Python 3.10: TypeError: 'type' object is not subscriptable
Python 3.11+: TypeError: type 'CustomType' is not subscriptable
and is_backport_fixable_error matches the first error message. This makes things even harder to manage unfortunately :/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In other words, there's no way to distinguish between a TypeError when a builtin/stdlib class is used (e.g. list[int], collections.abc.Sequence[str]) and other classes that are truly non subscriptable
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah gotcha. I'm ok with this approach then, but let's see what @alexmojaki thinks, as he's pretty well versed in the eval_type_backport space.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've pushed some changes which I think is the best we can do
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest keeping is_backport_fixable_error, and also updating is_not_subscriptable_error to check for Python <= 3.8 and is_unsupported_types_for_union_error for Python <= 3.9 (double check those Python versions). The newer type 'CustomType' is not subscriptable format isn't backport fixable because the backport only applies to old versions. So you can definitely make a better guess about whether it's worth suggesting installing eval_type_backport.
pydantic/_internal/_typing_extra.py
Outdated
| except TypeError as e: | ||
| raise TypeError( | ||
| f'Unable to evaluate type annotation {value.__forward_arg__!r}. ' | ||
| "It might be that the type being used isn't subscriptable." |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think it makes sense to mention the type not being subscriptable. That should already be in the traceback with the full original error message.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately only on 3.10+ as I mentioned here :/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So doing something like this on 3.8/3.9:
from __future__ import annotations
class CustomType:
pass
class Model(BaseModel):
foo: CustomType[int]is quite hard to debug, as you only get TypeError: 'type' object is not subscriptable
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But you will still have Unable to evaluate type annotation 'CustomType[int]'.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah yes makes sense
|
This doesn't really fix #8291. The user still gets an error, they just know where it's coming from. They have no idea what to do about it. If we expect them to do some workaround such as #8291 (comment) then that should be suggested, after checking that the problem appears to be subscripting. |
pydantic/_internal/_typing_extra.py
Outdated
| sys.version_info <= (3, 8) | ||
| and msg.startswith('unsupported operand type(s) for |: ') | ||
| or sys.version_info <= (3, 9) | ||
| and "' object is not subscriptable" in msg |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These versions look backwards, and that <= looks wrong.
➜ ~ python3.9
Python 3.9.19 (main, Aug 2 2024, 16:30:42)
[Clang 15.0.0 (clang-1500.3.9.4)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> int | str
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for |: 'type' and 'type'
>>> list[int]
list[int]
>>> import sys
>>> sys.version_info
sys.version_info(major=3, minor=9, micro=19, releaselevel='final', serial=0)
>>> tuple(sys.version_info)
(3, 9, 19, 'final', 0)
>>> sys.version_info <= (3, 9)
False
>>> (3, 8, 0) <= (3, 8)
False
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed comparing with <= is confusing.
As a side note, I've raised microsoft/pyright#8648.
Not sure we can hint anything to the user about this. Even if the issue is about subscripting, it could be that:
Defining a dummy |
pydantic/_internal/_typing_extra.py
Outdated
| '`typing` constructs or install the `eval_type_backport` package.' | ||
| ) from e | ||
|
|
||
| return eval_type_backport(value, globalns, localns, try_default=False) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On Python 3.8, we will still have the unclear TypeError: 'type' object is not subscriptable. We could add yet another try/expect block but this starts to really hurt readability here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll try to figure out a better way
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
def eval_type_with_helpful_errors(
value: Any,
globalns: dict[str, Any] | None = None,
localns: dict[str, Any] | None = None,
type_params: tuple[Any] | None = None,
) -> Any:
try:
return _eval_type_with_backport_package(value, globalns, localns, type_params)
except Exception as e:
message = f'Unable to evaluate type annotation {value.__forward_arg__!r}.'
if sys.version_info >= (3, 11):
e.add_note(message)
else:
raise TypeError(message) from e
def _eval_type_with_backport_package(
value: Any,
globalns: dict[str, Any] | None = None,
localns: dict[str, Any] | None = None,
type_params: tuple[Any] | None = None,
) -> Any:
try:
return _eval_type_with_type_params(value, globalns, localns, type_params)
except TypeError as e:
if not (isinstance(value, typing.ForwardRef) and is_backport_fixable_error(e)):
raise
try:
from eval_type_backport import eval_type_backport
except ImportError:
raise TypeError(
f'Unable to evaluate type annotation {value.__forward_arg__!r}. If you are making use '
'of the new typing syntax (unions using `|` since Python 3.10 or builtins subscripting '
'since Python 3.9), you should either replace the use of new syntax with the existing '
'`typing` constructs or install the `eval_type_backport` package.'
) from e
return eval_type_backport(value, globalns, localns, try_default=False)
def _eval_type_with_type_params(
value: Any,
globalns: dict[str, Any] | None = None,
localns: dict[str, Any] | None = None,
type_params: tuple[Any] | None = None,
) -> Any:
if sys.version_info >= (3, 13):
return typing._eval_type( # type: ignore
value, globalns, localns, type_params=type_params
)
else:
return typing._eval_type( # type: ignore
value, globalns, localns
)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Viicos I suggested except Exception in eval_type_with_helpful_errors so that there would be helpful notes for other errors. We're seeing a mysterious NameError: name 'Dict' is not defined and we don't know where it's coming from. The final code in the PR is still only catching TypeError so no note is added.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The thing is eval_type_backport is used by eval_type_lenient (defined above), and is the one responsible for catching (and not erroring) NameErrors
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But eval_type_lenient is not the only place that calls eval_type_backport, so NameError can still bubble up.
raise type(e)(message) from e should work.
|
Thanks @alexmojaki for the feedback. Implementation isn't that great but I don't expect this code to change (hopefully). Once we drop support for 3.8 and 3.9, it will be simplified a lot anyway. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome work here 🚀 !
Related issue number
Fixes #8291.
I first wanted to have the actual field name displayed (and possibly line number). This could be done in
get_cls_type_hints_lenient(which callseval_type_lenientfor each annotation) where we have access to the field names, but this is only used byBaseModel, not Pydantic dataclasses,validate_call, etc.But I think at least providing the bad annotation string repr as it is done in this PR could be sufficient: users just need to Ctrl + F through the code to find where it is coming from
Checklist