Repository navigation
fix: support PEP 810 global lazy imports - #1735
Merged
Merged
Conversation
On Python 3.15 with `PYTHON_LAZY_IMPORTS=all`, `pact_ffi.ffi` resolved to the `pact_ffi.ffi` extension module instead of the cffi `FFI` object, so the first FFI call raised `AttributeError: module 'pact_ffi.ffi' has no attribute 'NULL'`. `pact_ffi.__version__` did the same when `__version_tuple__` was read first. Both submodules share their names with attributes the package re-exports. Under PEP 810, `from pact_ffi.ffi import ffi` binds `ffi` lazily. Loading the submodule later sets the package attribute `ffi` to the module, replacing the object. The package now imports each submodule under an alias and assigns the attributes from it, so the binding happens after the submodule has loaded. Assisted-by: Claude Code:claude-opus-5-5
On Python 3.15 with `PYTHON_LAZY_IMPORTS=all`, two of the attributes `pact` documents break. - `pact.match` and `pact.generate` raised `AttributeError`. `pact/__init__.py` never imported them: they became attributes only as a side effect of `pact.xml` importing them, and a lazy import has no side effects until it is used. They are now imported explicitly. - `pact.__version__` resolved to the `pact.__version__` submodule when `__version_tuple__` was read first, because the submodule shares the attribute's name. The package now imports the submodule under an alias and assigns both attributes from it, as `pact_ffi` does. Without lazy imports, every module involved was already imported, so the explicit imports add no cost. Assisted-by: Claude Code:claude-opus-5-5
`tests/test_lazy_imports.py` checks, in a subprocess with `-X lazy_imports=all`, that the public attributes of `pact` and `pact_ffi` keep their types and that an FFI call works. It reads `__version_tuple__` first, the access order that exposed the `__version__` collision. pytest cannot run with `PYTHON_LAZY_IMPORTS=all`: its own lazily imported `fnmatch` raises `ImportCycleError`. Setting `PACT_TEST_LAZY_IMPORTS` instead makes only imports issued by `pact` and `pact_ffi` modules lazy. A new CI step runs the suite that way on Python 3.15 on Linux. Assisted-by: Claude Code:claude-opus-5-5
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #1735 +/- ##
====================================
Coverage 68% 68%
====================================
Files 20 20
Lines 3288 3295 +7
====================================
+ Hits 2261 2268 +7
Misses 1027 1027
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. |
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.
📝 Summary
On Python 3.15, Pact breaks for users who turn on PEP 810 lazy imports globally with
PYTHON_LAZY_IMPORTS=allor-X lazy_imports=all. Three attributes resolve to the wrong value:lazy_imports=allpact_ffi.ffipact_ffi.ffiextension module, so the first FFI call raisesAttributeError: module 'pact_ffi.ffi' has no attribute 'NULL'pact.__version__,pact_ffi.__version____version__submodule, whenever__version_tuple__is read firstpact.match,pact.generateAttributeErrorpact.xmlimporting themThe name collisions.
from pact_ffi.ffi import ffibindsffilazily. When the submodule loads later, the import system sets the package attributeffito the module, replacing the object. Each package now imports the colliding submodule under an alias and assigns the attribute from it, so the binding happens after the submodule has loaded.The missing attributes.
pact/__init__.pynow importsmatchandgenerateexplicitly. Without lazy imports, both were already imported, so this adds no cost.The
pact_ffifix reaches users with the nextpact-python-ffirelease.🔥 Motivation
PEP 810 makes global lazy mode a user choice, so pact should work under it.
🔨 Test Plan
tests/test_lazy_imports.pyruns on Python 3.15 and is skipped on older versions. In a subprocess with-X lazy_imports=all, it checks that each public attribute keeps its type, reading__version_tuple__first. It also checks that an FFI call works.PYTHON_LAZY_IMPORTS=all: its lazily importedfnmatchraisesImportCycleError. SettingPACT_TEST_LAZY_IMPORTS=1instead makestests/conftest.pylazy-load only the imports issued bypactandpact_ffimodules. A new CI step runs the suite that way on Python 3.15, Linux only.PACT_TEST_LAZY_IMPORTS=1): the suite passes, 536 passed and 14 skipped.main: the same run gives 146 failures intests/test_pact.pyalone, and the new regression tests fail 6 of 8.🔗 Related issues/PRs
Merges independently of #1734; the two branches touch no common files.