Skip to content

Commit 4418e5e

Browse files
ashbjcrist
andauthored
Use eval_type_backport on Python 3.9 if it's installed to resolve int | None etc. (#773)
* Use eval_type_backport on Python 3.9 if it's installed to resolve `int | 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.) --------- Co-authored-by: Jim Crist-Harif <[email protected]>
1 parent 3c487c1 commit 4418e5e

File tree

5 files changed

+72
-7
lines changed

5 files changed

+72
-7
lines changed

.github/workflows/ci.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ jobs:
2929

3030
- name: Build msgspec and install dependencies
3131
run: |
32-
pip install coverage -e ".[test]"
32+
pip install -e ".[dev]"
3333
3434
- name: Run pre-commit hooks
3535
uses: pre-commit/[email protected]
@@ -78,7 +78,7 @@ jobs:
7878
os: [ubuntu-latest, macos-13, windows-latest]
7979

8080
env:
81-
CIBW_TEST_REQUIRES: "pytest msgpack pyyaml tomli tomli_w"
81+
CIBW_TEST_EXTRAS: "test"
8282
CIBW_TEST_COMMAND: "pytest {project}/tests"
8383
CIBW_BUILD: "cp39-* cp310-* cp311-* cp312-* cp313-*"
8484
CIBW_SKIP: "*-win32 *_i686 *_s390x *_ppc64le"
@@ -102,7 +102,7 @@ jobs:
102102
echo "CIBW_SKIP=${CIBW_SKIP} *-musllinux_* cp39-*_aarch64 cp311-*_aarch64 cp312-*_aarch64 cp313-*_aarch64" >> $GITHUB_ENV
103103
104104
- name: Build & Test Wheels
105-
uses: pypa/cibuildwheel@v2.21.3
105+
uses: pypa/cibuildwheel@v2.22.0
106106

107107
- name: Upload artifact
108108
uses: actions/upload-artifact@v4

msgspec/_utils.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,28 @@ def _forward_ref(value):
4545

4646
def _eval_type(t, globalns, localns):
4747
return typing._eval_type(t, globalns, localns, ())
48+
elif sys.version_info < (3, 10):
49+
50+
def _eval_type(t, globalns, localns):
51+
try:
52+
return typing._eval_type(t, globalns, localns)
53+
except TypeError as e:
54+
try:
55+
from eval_type_backport import eval_type_backport
56+
except ImportError:
57+
raise TypeError(
58+
f"Unable to evaluate type annotation {t.__forward_arg__!r}. If you are making use "
59+
"of the new typing syntax (unions using `|` since Python 3.10 or builtins subscripting "
60+
"since Python 3.9), you should either replace the use of new syntax with the existing "
61+
"`typing` constructs or install the `eval_type_backport` package."
62+
) from e
63+
64+
return eval_type_backport(
65+
t,
66+
globalns,
67+
localns,
68+
try_default=False,
69+
)
4870
else:
4971
_eval_type = typing._eval_type
5072

setup.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,15 @@
5151
yaml_deps = ["pyyaml"]
5252
toml_deps = ['tomli ; python_version < "3.11"', "tomli_w"]
5353
doc_deps = ["sphinx", "furo", "sphinx-copybutton", "sphinx-design", "ipython"]
54-
test_deps = ["pytest", "mypy", "pyright", "msgpack", "attrs", *yaml_deps, *toml_deps]
55-
dev_deps = ["pre-commit", "coverage", "gcovr", *doc_deps, *test_deps]
54+
test_deps = [
55+
"pytest",
56+
"msgpack",
57+
"attrs",
58+
'eval-type-backport ; python_version < "3.10"',
59+
*yaml_deps,
60+
*toml_deps,
61+
]
62+
dev_deps = ["pre-commit", "coverage", "mypy", "pyright", *doc_deps, *test_deps]
5663

5764
extras_require = {
5865
"yaml": yaml_deps,

tests/test_utils.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
from __future__ import annotations
22

3-
from typing import Generic, List, Set, TypeVar
3+
import sys
4+
from typing import Generic, List, Optional, Set, TypeVar
45

56
import pytest
6-
from utils import temp_module
7+
from utils import temp_module, package_not_installed
78

89
from msgspec._utils import get_class_annotations
910

11+
PY310 = sys.version_info[:2] >= (3, 10)
12+
1013
T = TypeVar("T")
1114
S = TypeVar("S")
1215
U = TypeVar("U")
@@ -201,3 +204,23 @@ class Sub(Base[Invalid]):
201204
pass
202205

203206
assert get_class_annotations(Sub) == {"x": Invalid}
207+
208+
@pytest.mark.skipif(PY310, reason="<3.10 only")
209+
def test_union_backport_not_installed(self):
210+
class Ex:
211+
x: int | None = None
212+
213+
with package_not_installed("eval_type_backport"):
214+
with pytest.raises(
215+
TypeError, match=r"or install the `eval_type_backport` package."
216+
):
217+
get_class_annotations(Ex)
218+
219+
@pytest.mark.skipif(PY310, reason="<3.10 only")
220+
def test_union_backport_installed(self):
221+
class Ex:
222+
x: int | None = None
223+
224+
pytest.importorskip("eval_type_backport")
225+
226+
assert get_class_annotations(Ex) == {"x": Optional[int]}

tests/utils.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,16 @@ def max_call_depth(n):
4545
yield
4646
finally:
4747
sys.setrecursionlimit(orig)
48+
49+
50+
@contextmanager
51+
def package_not_installed(name):
52+
try:
53+
orig = sys.modules.get(name)
54+
sys.modules[name] = None
55+
yield
56+
finally:
57+
if orig is not None:
58+
sys.modules[name] = orig
59+
else:
60+
del sys.modules[name]

0 commit comments

Comments
 (0)