Python 3.9 compatibility#1743
Open
NN--- wants to merge 1 commit into
Open
Conversation
Contributor
PR governanceThis PR does not yet satisfy the required template fields:
Please update the PR body, or move the PR back to draft while it is still in progress. |
Lower the minimum supported Python from 3.10 to 3.9.
- pyproject: requires-python >=3.9, 3.9 classifier, ruff/mypy 3.9 targets;
eval-type-backport lets pydantic evaluate PEP 604 string annotations on 3.9
- Gate 3.10-only deps (litellm, mcp, torch, tree-sitter, langchain-*,
strands-agents) behind python_version >= '3.10' markers; all their uses
were already lazy/ImportError-guarded
- PyO3: abi3-py310 -> abi3-py39 (wheel now installs on 3.9+)
- Add headroom/_compat.py:
- aclosing, entry_points_group (3.10-only stdlib APIs)
- AsyncLock/AsyncEvent/AsyncSemaphore/AsyncQueue: 3.9 asyncio primitives
bind get_event_loop() eagerly at construction; these backport 3.10's
lazy binding so sync-context construction works and locks attach to
the loop they are awaited in
- DATACLASS_SLOTS: @DataClass(**DATACLASS_SLOTS) applies slots=True on
3.10+ only
- Fix real 3.9 bugs found by the suite: time.monotonic() starts near 0 on
macOS 3.9, so 0.0 'never happened' sentinels wrongly throttled the first
seconds of process life (codex_rate_limits, traffic_learner); bare
tomllib imports (3.11+) in tests/scripts
- Rewrite runtime PEP 604 unions (isinstance tuples, cast Optional/Union),
add future annotations imports where missing, drop zip(strict=)
- CI: add py39-smoke job (installs the abi3 wheel on 3.9, imports, CLI)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Lower the minimum supported Python from 3.10 to 3.9.
Most of the codebase was already 3.9-clean at the syntax level (thanks to
from __future__ import annotations); the real work was runtime semantics. This PR adds a smallheadroom/_compat.pyshim module, gates 3.10-only dependencies behind environment markers, and fixes two genuine 3.9 runtime bugs the test suite exposed.Closes #1744
Type of Change
Changes Made
pyproject.toml:requires-python = ">=3.9", 3.9 classifier,ruff target-version = "py39",mypy python_version = "3.9",eval-type-backportdep (lets pydantic evaluate PEP 604 string annotations on 3.9)python_version >= '3.10'markers: litellm, mcp, torch, tree-sitter(-language-pack), langchain-core/openai/ollama, strands-agents, huggingface-hub pin. All uses were already lazy / ImportError-guarded (same pattern as the existing 3.14 litellm gate, GH [BUG] compression_first_stage always times out (30s) on Python 3.14 + headroom-ai 0.25.0 incompatible with Python 3.14 (litellm dep) #956)Cargo.toml: PyO3abi3-py310→abi3-py39— one wheel now installs on 3.9–3.14headroom/_compat.py:aclosing,entry_points_group()— 3.10-only stdlib APIsAsyncLock/AsyncEvent/AsyncSemaphore/AsyncQueue— Python 3.9 asyncio primitives bindget_event_loop()eagerly at construction; these backport 3.10's lazy binding so sync-context construction works and primitives attach to the loop they are awaited in (23 construction sites migrated)DATACLASS_SLOTS—@dataclass(**DATACLASS_SLOTS)appliesslots=Trueon 3.10+, no-op on 3.9time.monotonic()starts near 0 at process start, so0.0"never happened" sentinels wrongly throttled polls/flushes during early process life (subscription/codex_rate_limits.py,memory/traffic_learner.py); bareimport tomllib(3.11+) in tests/scriptsisinstance(x, A | B)→ tuples,cast(X | None, …)→Optional[…], added missingfrom __future__ import annotations, removedzip(strict=)py39-smokejob — installs the abi3 wheel on 3.9, importsheadroom, runs the CLI, byte-compiles the installed packageTesting
pytest)ruff check .)mypy headroom)Test Output
Real Behavior Proof
uv python install 3.9, Rust 1.95.0, wheel built with maturin (abi3-py39)uv venv --python 3.9 && uv pip install dist/headroom_ai-0.29.0-cp39-abi3-macosx_11_0_arm64.whlpython -c "import headroom; from headroom._core import hello; print(headroom.__version__, hello())"pkgutil.walk_packagesover the installed package, importing all modulespython -m pytest tests -qon the 3.9 venv (dev+proxy extras)uv pip compile pyproject.toml --python-version 3.9 --extra <each of proxy,code,ml,memory,vector,relevance,image,dev,all>— all resolve;uv lockregeneratedheadroom 0.29.0+ Rust coreheadroom-coreload; 398 modules import, 0 hard failures (4 expected optional-dep skips: litellm/mcp/trafilatura); 7139 tests pass; every extra resolves on both 3.9 and 3.12py39-smokejob); GPU/torch paths on 3.9 (torch is gated to 3.10+ by its own wheels); live provider integration tests requiring API keysReview Readiness
Checklist
Additional Notes
[proxy]installs withoutmcp(MCP CLI prints an install hint, code is ImportError-guarded),[ml]installs transformers 4.x without torch,[code]no-ops without tree-sitter — matching the existing 3.14/litellm degradation pattern.tests/monkeypatch targets moved fromimportlib.metadata.entry_pointsto the newentry_points_groupseam where needed;_compatresolvesimportlib.metadata.entry_pointsdynamically so existing global patches keep working on 3.10+.