diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4dffdef..1678b96 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,16 +8,17 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Set up Python 3.8 + - name: Set up Python 3.12 uses: actions/setup-python@v4 with: - python-version: 3.8 + python-version: "3.12" - name: cache poetry install uses: actions/cache@v4 with: path: ~/.local - key: poetry-1.4.0-0 + # Include Python version to avoid reusing a Poetry self-venv built for a different interpreter (e.g. 3.8 -> missing libpython3.8.so) + key: poetry-1.4.0-0-py312 - name: Install Poetry uses: snok/install-poetry@v1 @@ -38,7 +39,9 @@ jobs: if: steps.cache-deps.outputs.cache-hit != 'true' - run: poetry install --no-interaction - - run: pip install pydantic==1.* + + - name: Install Pydantic v2 for testing + run: poetry run pip install pydantic==2.11.7 - name: Test with pytest - run: poetry run pytest tests/pydantic_v1 tests/common + run: poetry run pytest tests/pydantic_v2 tests/common diff --git a/docs/changelog.md b/docs/changelog.md index fb5cfe7..7227330 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,6 +2,11 @@ ## v1.13 +### v1.13.1 (2025.8.28) + +- fix: add support for UnionType such as `A | B` +- update: upgrade python version to 3.10 in ci + ### v1.13.0 (2025.8.27) feature: diff --git a/pydantic_resolve/utils/types.py b/pydantic_resolve/utils/types.py index 7bf4e9b..ea57d48 100644 --- a/pydantic_resolve/utils/types.py +++ b/pydantic_resolve/utils/types.py @@ -1,4 +1,8 @@ from typing import Type, Union, List +try: # Python 3.10+ provides PEP 604 unions using types.UnionType + from types import UnionType as _UnionType +except ImportError: # pragma: no cover - prior to 3.10 + _UnionType = () # sentinel so membership tests still work from pydantic_resolve.compat import PYDANTIC_V2, OVER_PYTHON_3_7 if OVER_PYTHON_3_7: @@ -65,16 +69,17 @@ def _shell_list(_tp): while True: orig = _get_origin(tp) - if orig is Union: + + if orig in (Union, _UnionType): args = list(_get_args(tp)) non_none = [a for a in args if a is not type(None)] # noqa: E721 has_none = len(non_none) != len(args) - # Optional[T] case -> keep unwrapping + # Optional[T] case -> keep unwrapping (exactly one real type + None) if has_none and len(non_none) == 1: tp = non_none[0] tp = _shell_list(tp) continue - # Union (with or without None) -> return tuple of real types + # General union: return all non-None members (order preserved) if non_none: return tuple(non_none) return tuple() diff --git a/pyproject.toml b/pyproject.toml index 6304a16..6d7e56f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "pydantic-resolve" -version = "1.13.0" +version = "1.13.1" description = "A business model friendly data orchestration tool, simple way to implement the core concept of clean architecture." authors = ["tangkikodo "] readme = "README.md" diff --git a/tests/common/test_types.py b/tests/common/test_types.py index 81198ec..e80f9c2 100644 --- a/tests/common/test_types.py +++ b/tests/common/test_types.py @@ -107,7 +107,6 @@ def test_is_list(annotation, expected): ) def test_get_core_types(tp, expected): result = get_core_types(tp) - print(result) assert result == expected diff --git a/tests/common/test_union_types.py b/tests/common/test_union_types.py new file mode 100644 index 0000000..b99e6b1 --- /dev/null +++ b/tests/common/test_union_types.py @@ -0,0 +1,33 @@ + +from pydantic_resolve.utils.types import ( + get_core_types, +) +from typing import Optional, List +import pytest + +@pytest.mark.parametrize( + "tp,expected", + [ + + # Optional types + (int | None, (int,)), + (None | int, (int,)), + + # Union types (multiple non-None types) + (int | str, (int, str)), + (int | str | float, (int, str, float)), + (str | int, (str, int)), # Order matters + + # Union with None + (int | str | None, (int, str)), + (None | int | str, (int, str)), + (int | None | str, (int, str)), + + # Complex nested types + (List[int | str], (int, str)), + (Optional[List[int | str]], (int, str)), + ] +) +def test_get_core_types_3_10(tp, expected): + result = get_core_types(tp) + assert result == expected \ No newline at end of file diff --git a/tests/pydantic_v2/resolver/test_44_union.py b/tests/pydantic_v2/resolver/test_44_union.py index b6f780f..fc2dacf 100644 --- a/tests/pydantic_v2/resolver/test_44_union.py +++ b/tests/pydantic_v2/resolver/test_44_union.py @@ -1,4 +1,4 @@ -from typing import Union +from typing import Union, List from pydantic import BaseModel from pydantic_resolve import Resolver, Loader import pytest @@ -35,7 +35,7 @@ class C(BaseModel): class Container(BaseModel): - items: list[Item] = [] + items: List[Item] = [] def resolve_items(self): return [A(id='1'), B(id='2', name='Item 2')] diff --git a/tox.ini b/tox.ini index fae118f..fbb6dfe 100644 --- a/tox.ini +++ b/tox.ini @@ -25,7 +25,8 @@ basepython = python3.8 commands_pre = poetry install --sync commands = - pytest tests/pydantic_v1 tests/common + # Python <3.10 can't parse PEP 604 union syntax (e.g. int | None); ignore that test file + pytest --ignore=tests/common/test_union_types.py tests/pydantic_v1 tests/common [testenv:py38pyd2] commands_pre = @@ -33,14 +34,14 @@ commands_pre = pip install pydantic==2.* basepython = python3.8 commands = - pytest tests/pydantic_v2 tests/common + pytest --ignore=tests/common/test_union_types.py tests/pydantic_v2 tests/common [testenv:py39pyd1] basepython = python3.9 commands_pre = poetry install --sync commands = - pytest tests/pydantic_v1 tests/common + pytest --ignore=tests/common/test_union_types.py tests/pydantic_v1 tests/common [testenv:py39pyd2] commands_pre = @@ -48,7 +49,7 @@ commands_pre = pip install pydantic==2.* basepython = python3.9 commands = - pytest tests/pydantic_v2 tests/common + pytest --ignore=tests/common/test_union_types.py tests/pydantic_v2 tests/common [testenv:py310pyd1] basepython = python3.10