Skip to content

Commit

Permalink
Use eval_type_backport on Python 3.9 if it's installed to resolve `in…
Browse files Browse the repository at this point in the history
…t | None` etc.

This uses the same module that pydantic does, and it allows people to use the
new pipe syntax if they have to support Python3.9 too -- very useful for
libraries.

(Also it works better with many type checkers which seem to mistakenly think
that with `from __future__ import annotations` means `int| None` will work,
but it doesn't out of the box.)
  • Loading branch information
ashb committed Nov 16, 2024
1 parent 595c33c commit 3cc6d81
Show file tree
Hide file tree
Showing 2 changed files with 49 additions and 1 deletion.
22 changes: 22 additions & 0 deletions msgspec/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,28 @@ def _forward_ref(value):

def _eval_type(t, globalns, localns):
return typing._eval_type(t, globalns, localns, ())
elif sys.version_info < (3, 10):

def _eval_type(t, globalns, localns):
try:
return typing._eval_type(t, globalns, localns)
except TypeError as e:
try:
from eval_type_backport import eval_type_backport
except ImportError:
raise TypeError(
f"Unable to evaluate type annotation {t.__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(
t,
globalns,
localns,
try_default=False,
)
else:
_eval_type = typing._eval_type

Expand Down
28 changes: 27 additions & 1 deletion tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
from __future__ import annotations

from typing import Generic, List, Set, TypeVar
import contextlib
import sys
from typing import Generic, List, Optional, Set, TypeVar

import pytest
from utils import temp_module

from msgspec._utils import get_class_annotations

PY310 = sys.version_info[:2] >= (3, 10)

T = TypeVar("T")
S = TypeVar("S")
U = TypeVar("U")
Expand Down Expand Up @@ -201,3 +205,25 @@ class Sub(Base[Invalid]):
pass

assert get_class_annotations(Sub) == {"x": Invalid}

@pytest.mark.skipif(PY310, reason="<3.10 only")
@pytest.mark.parametrize(
("matcher"),
[
# Installed, should give us the result to check
pytest.param(contextlib.nullcontext(), id="installed"),
# Not installed, sohuld throw this error
pytest.param(
pytest.raises(
TypeError, match=r"or install the `eval_type_backport` package."
),
id="not_installed",
),
],
)
def test_union_backcompat(self, matcher):
class X:
opt_int: int | None = None

with matcher:
assert get_class_annotations(X) == {"opt_int": Optional[int]}

0 comments on commit 3cc6d81

Please sign in to comment.