Skip to content

Conversation

Viicos
Copy link
Member

@Viicos Viicos commented Aug 2, 2024

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 calls eval_type_lenient for each annotation) where we have access to the field names, but this is only used by BaseModel, 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

  • The pull request title is a good summary of the changes - it will be used in the changelog
  • Unit tests for the changes exist
  • Tests pass on CI
  • Documentation reflects the changes where applicable
  • My PR is ready to review, please add a comment including the phrase "please review" to assign reviewers

@github-actions github-actions bot added the relnotes-fix Used for bugfixes. label Aug 2, 2024
Copy link

codspeed-hq bot commented Aug 2, 2024

CodSpeed Performance Report

Merging #10030 will not alter performance

Comparing Viicos:eval-type-error (2aef05f) with main (ddc8f58)

Summary

✅ 17 untouched benchmarks

Copy link
Contributor

@sydney-runkle sydney-runkle left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Viicos,

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?

) from e

return eval_type_backport(value, globalns, localns, try_default=False)
if isinstance(value, typing.ForwardRef):
Copy link
Contributor

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?

Copy link
Member Author

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 :/

Copy link
Member Author

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

Copy link
Contributor

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.

Copy link
Member Author

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

Copy link
Contributor

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.

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."
Copy link
Contributor

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.

Copy link
Member Author

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 :/

Copy link
Member Author

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

Copy link
Contributor

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]'.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes makes sense

@alexmojaki
Copy link
Contributor

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.

Comment on lines 295 to 322
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
Copy link
Contributor

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

Copy link
Member Author

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.

@Viicos
Copy link
Member Author

Viicos commented Aug 4, 2024

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.

Not sure we can hint anything to the user about this. Even if the issue is about subscripting, it could be that:

  • it is generic only in stubs (as it was in the original issue)
  • it isn't generic at all and the user did a mistake

Defining a dummy __class_getitem__ can work but at this point I would expect the user to know what he is doing, especially because these kind of errors can only happen with custom types.

'`typing` constructs or install the `eval_type_backport` package.'
) from e

return eval_type_backport(value, globalns, localns, try_default=False)
Copy link
Member Author

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.

Copy link
Member Author

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

Copy link
Contributor

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
        )

Copy link
Contributor

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.

Copy link
Member Author

@Viicos Viicos Aug 6, 2024

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

Copy link
Contributor

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.

@Viicos
Copy link
Member Author

Viicos commented Aug 5, 2024

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.

Copy link
Contributor

@sydney-runkle sydney-runkle left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome work here 🚀 !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
awaiting author revision awaiting changes from the PR author relnotes-fix Used for bugfixes.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Classes that are generic in stubs but not at runtime causes TypeError
3 participants