alexmojaki / eval_type_backport

Like `typing._eval_type`, but lets older Python versions use newer typing features.
MIT License
11 stars 2 forks source link

How to use it #11

Open SylvainGuieu opened 10 months ago

SylvainGuieu commented 10 months ago

Hi, I fill very stupid but ... I followed the issues on pydantic I end-up here, I installed it, but I have no idea how to use it !

What should I do with the eval_type_backport to make it work with pydantic ? I though I could replace typing._eval_type method to this one by I end up with cycling calls.

Could you drop a few lines in the Readme ?

cheers, Sylvain

bswck commented 10 months ago

Hello @SylvainGuieu, As you might have noticed, pydantic/pydantic#8209 is still open. That means you cannot use eval_type_backport in pydantic easily just yet, because it's not merged and released.

When it comes to using eval_type_backport, I'll write a few lines in the readme soon.

As for now, you might use my (a bit worse) solution https://github.com/bswck/modern_types which simply requires import __modern_types__ line on top of your module and you're all set. If it doesn't work for you, let me know.

SylvainGuieu commented 10 months ago

@bswck Thanks, I will try this.

alexmojaki commented 10 months ago

Thanks @bswck for responding.

Merging https://github.com/pydantic/pydantic/pull/8209 is indeed the main thing that needs to happen. Once that's released you shouldn't need to do anything from pydantic as long as this package is installed.

I thought I could replace typing._eval_type method to this one by I end up with cycling calls.

I think the fact that you can't do this should be considered a bug. It'd also be good for this package to expose some API which does this for you. Then this can easily be used both before whatever version of pydantic releases this and outside pydantic.

alexmojaki commented 10 months ago

The pydantic PR is merged, and pydantic 2.6.0 beta should be released soon.

pawamoy commented 6 months ago

Hey @bswck (@alexmojaki), I see you wanted to improve the docs?

May I send a quick PR here to show in the README how to use this? I was confused too and thought it would override typing._eval_type, but you actually have to from eval_type_backport import eval_type_backport.

pawamoy commented 6 months ago

Huh actually I'm still not managing to use it correctly :thinking:

With

eval_type(  # noqa: PGH001,S307
    param.annotation,
    exec_globals,
    {},
    try_default=False,
)

...param.annotation being "str | None"

    def _eval_direct(
        value: typing.ForwardRef,
        globalns: dict[str, Any] | None = None,
        localns: dict[str, Any] | None = None,
    ):
>       tree = ast.parse(value.__forward_arg__, mode='eval')
E       AttributeError: 'str' object has no attribute '__forward_arg__'

I suppose I should manually wrap any string annotation into a forward ref?

pawamoy commented 6 months ago

Yep, this seems to work:

eval_type(  # noqa: PGH001,S307
    ForwardRef(param.annotation) if isinstance(param.annotation, str) else param.annotation,
    exec_globals,
    {},
)

With this I can also drop try_default=False and let eval-type-backport do its thing. Which also means I can drop this:

try:
    from eval_type_backport import eval_type_backport as eval_type
except ImportError:
    from typing import _eval_type

    def eval_type(*args, **kwargs):
        kwargs.pop("try_default", None)
        return _eval_type(*args, **kwargs)

:+1:

Would be nice though it eval-type-backport would cast strings to forward refs automatically (if that makes sense) :slightly_smiling_face:

alexmojaki commented 6 months ago

try_default=False is not really meant for 'public' use. Here's how it's used in pydantic:


def eval_type_backport(
    value: Any, globalns: dict[str, Any] | None = None, localns: dict[str, Any] | None = None
) -> Any:
    """Like `typing._eval_type`, but falls back to the `eval_type_backport` package if it's
    installed to let older Python versions use newer typing features.
    Specifically, this transforms `X | Y` into `typing.Union[X, Y]`
    and `list[X]` into `typing.List[X]` etc. (for all the types made generic in PEP 585)
    if the original syntax is not supported in the current Python version.
    """
    try:
        return typing._eval_type(  # type: ignore
            value, globalns, localns
        )
    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'You have a type annotation {value.__forward_arg__!r} '
                f'which makes use of newer typing features than are supported in your version of Python. '
                f'To handle this error, you should either remove the use of new syntax '
                f'or install the `eval_type_backport` package.'
            ) from e

        return eval_type_backport(value, globalns, localns, try_default=False)

def is_backport_fixable_error(e: TypeError) -> bool:
    msg = str(e)
    return msg.startswith('unsupported operand type(s) for |: ') or "' object is not subscriptable" in msg

Notice how a bunch of code is almost identical to eval_type_backport itself so that it only suggests installing the package if it might be useful. try_default=False is just there to prevent calling typing._eval_type a second time to save a bit of time since it's already been checked and failed.

It probably would have been better to just use _eval_direct in pydantic, but it's a bit late to change it now.

Would be nice though it eval-type-backport would cast strings to forward refs automatically (if that makes sense) 🙂

That would make it behave differently from typing._eval_type.

pawamoy commented 6 months ago

Thanks for your quick reply :slightly_smiling_face:

Here's how I now use eval-type-backport: https://github.com/pawamoy/duty/commit/e8ca7c1fb453a6f0b3de3268e2cea3434985c428. Let me know if you'd like me to send a PR to show quick usage in the readme.

JPHutchins commented 6 months ago

Wanted to chime in that this "just worked" after updating pydantic.

Excerpt from pyproject.toml

[tool.poetry.dependencies]
python = ">=3.9, <3.13"
pydantic = "^2.6"
eval-type-backport = { version = "^0.2.0", python = "<3.10"}