Skip to content
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

Use eval_type_backport on Python 3.9 if it's installed to resolve int | None etc. #773

Merged
merged 4 commits into from
Dec 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:

- name: Build msgspec and install dependencies
run: |
pip install coverage -e ".[test]"
pip install -e ".[dev]"

- name: Run pre-commit hooks
uses: pre-commit/action@v3.0.0
Expand Down Expand Up @@ -78,7 +78,7 @@ jobs:
os: [ubuntu-latest, macos-13, windows-latest]

env:
CIBW_TEST_REQUIRES: "pytest msgpack pyyaml tomli tomli_w"
CIBW_TEST_EXTRAS: "test"
CIBW_TEST_COMMAND: "pytest {project}/tests"
CIBW_BUILD: "cp39-* cp310-* cp311-* cp312-* cp313-*"
CIBW_SKIP: "*-win32 *_i686 *_s390x *_ppc64le"
Expand All @@ -102,7 +102,7 @@ jobs:
echo "CIBW_SKIP=${CIBW_SKIP} *-musllinux_* cp39-*_aarch64 cp311-*_aarch64 cp312-*_aarch64 cp313-*_aarch64" >> $GITHUB_ENV

- name: Build & Test Wheels
uses: pypa/cibuildwheel@v2.21.3
uses: pypa/cibuildwheel@v2.22.0

- name: Upload artifact
uses: actions/upload-artifact@v4
Expand Down
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
11 changes: 9 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,15 @@
yaml_deps = ["pyyaml"]
toml_deps = ['tomli ; python_version < "3.11"', "tomli_w"]
doc_deps = ["sphinx", "furo", "sphinx-copybutton", "sphinx-design", "ipython"]
test_deps = ["pytest", "mypy", "pyright", "msgpack", "attrs", *yaml_deps, *toml_deps]
dev_deps = ["pre-commit", "coverage", "gcovr", *doc_deps, *test_deps]
test_deps = [
"pytest",
"msgpack",
"attrs",
'eval-type-backport ; python_version < "3.10"',
*yaml_deps,
*toml_deps,
]
dev_deps = ["pre-commit", "coverage", "mypy", "pyright", *doc_deps, *test_deps]

extras_require = {
"yaml": yaml_deps,
Expand Down
27 changes: 25 additions & 2 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
from __future__ import annotations

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

import pytest
from utils import temp_module
from utils import temp_module, package_not_installed

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 +204,23 @@ class Sub(Base[Invalid]):
pass

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

@pytest.mark.skipif(PY310, reason="<3.10 only")
def test_union_backport_not_installed(self):
class Ex:
x: int | None = None

with package_not_installed("eval_type_backport"):
with pytest.raises(
TypeError, match=r"or install the `eval_type_backport` package."
):
get_class_annotations(Ex)

@pytest.mark.skipif(PY310, reason="<3.10 only")
def test_union_backport_installed(self):
class Ex:
x: int | None = None

pytest.importorskip("eval_type_backport")

assert get_class_annotations(Ex) == {"x": Optional[int]}
13 changes: 13 additions & 0 deletions tests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,16 @@ def max_call_depth(n):
yield
finally:
sys.setrecursionlimit(orig)


@contextmanager
def package_not_installed(name):
try:
orig = sys.modules.get(name)
sys.modules[name] = None
yield
finally:
if orig is not None:
sys.modules[name] = orig
else:
del sys.modules[name]
Loading