From 8fa2c8099ed7dcffab43d837bbeb5254b53310b4 Mon Sep 17 00:00:00 2001 From: Naz-Tac Date: Thu, 10 Sep 2026 20:03:26 -0700 Subject: [PATCH] fix(grpo): import on Python 3.9, the floor 2.1.2 declares hanzoai 2.1.2 declares requires-python >= 3.9, yet `import hanzoai` raises TypeError on 3.9. grpo/enhanced_api_model_adapter.py annotates four returns as `str | Tuple[str, Optional[str]]`, which 3.9 evaluates at class-body time, and hanzoai/__init__.py imports grpo eagerly. grpo/enhanced_semantic_extractor.py has the same shape once, behind it. Postpone annotation evaluation in both modules. They are the only two in the package whose PEP 604 unions reach the interpreter. Their dataclasses never resolve annotations, so nothing else changes. tests/test_imports.py imports every module under hanzoai. A new CI job runs it after `pip install .` on 3.9 and 3.13, because the test job only runs .python-version (3.12), where the annotation is valid. Co-Authored-By: Claude Opus 5 --- .github/workflows/ci.yml | 24 ++++++++++ .../grpo/enhanced_api_model_adapter.py | 2 + .../grpo/enhanced_semantic_extractor.py | 2 + tests/test_imports.py | 45 +++++++++++++++++++ 4 files changed, 73 insertions(+) create mode 100644 tests/test_imports.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3b286e5ae..611c91aea 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,3 +50,27 @@ jobs: - name: Run tests run: ./scripts/test + + # 2.1.2 declared requires-python >= 3.9 and could not be imported on 3.9, and + # the test job above only ever runs .python-version (3.12). Install the package + # the way a user does, on the floor and on the newest Python it classifies. + import: + name: import (Python ${{ matrix.python-version }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ['3.9', '3.13'] + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install the package + run: python -m pip install . pytest pytest-asyncio respx + + - name: Every module imports + run: python -m pytest tests/test_imports.py -q diff --git a/pkg/hanzoai/grpo/enhanced_api_model_adapter.py b/pkg/hanzoai/grpo/enhanced_api_model_adapter.py index 9be234f6f..66c759d93 100644 --- a/pkg/hanzoai/grpo/enhanced_api_model_adapter.py +++ b/pkg/hanzoai/grpo/enhanced_api_model_adapter.py @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations + import os import time import asyncio diff --git a/pkg/hanzoai/grpo/enhanced_semantic_extractor.py b/pkg/hanzoai/grpo/enhanced_semantic_extractor.py index 872dcc28c..e29ed5250 100644 --- a/pkg/hanzoai/grpo/enhanced_semantic_extractor.py +++ b/pkg/hanzoai/grpo/enhanced_semantic_extractor.py @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations + import os import copy import json diff --git a/tests/test_imports.py b/tests/test_imports.py new file mode 100644 index 000000000..c3bd7c517 --- /dev/null +++ b/tests/test_imports.py @@ -0,0 +1,45 @@ +"""Every module in ``hanzoai`` imports on every Python that requires-python admits. + +2.1.2 declared ``requires-python >= 3.9`` and could not be imported on 3.9 at all: +``grpo/enhanced_api_model_adapter.py`` annotated a return as ``str | Tuple[...]``, +which 3.9 evaluates at class-body time and rejects with ``TypeError``, and +``hanzoai/__init__.py`` imports ``grpo`` eagerly. The test job runs only +``.python-version`` (3.12), where that line is valid, so nothing could see it. + +The module list comes from the filesystem rather than from importing the package, +so a package that fails to import shows up as a failing module instead of an +empty list. +""" + +from __future__ import annotations + +import importlib +import importlib.util +from pathlib import Path + +import pytest + + +def _module_names() -> list[str]: + spec = importlib.util.find_spec("hanzoai") + assert spec is not None and spec.submodule_search_locations + root = Path(next(iter(spec.submodule_search_locations))) + names = [] + for path in sorted(root.rglob("*.py")): + parts = path.relative_to(root.parent).with_suffix("").parts + if parts[-1] == "__init__": + parts = parts[:-1] + names.append(".".join(parts)) + return names + + +@pytest.mark.parametrize("name", _module_names()) +def test_module_imports(name: str) -> None: + try: + importlib.import_module(name) + except ModuleNotFoundError as exc: + # An optional dependency that is not installed says nothing about this + # interpreter; a missing hanzoai module does. + if exc.name and exc.name.split(".")[0] != "hanzoai": + pytest.skip(f"optional dependency {exc.name} is not installed") + raise