Skip to content

fix: support PEP 810 global lazy imports - #1735

Merged
JP-Ellis merged 3 commits into
mainfrom
fix/lazy-import-compat
Oct 16, 2026
Merged

JP-Ellis merged 3 commits into
mainfrom
fix/lazy-import-compat

Conversation

@JP-Ellis

Copy link
Copy Markdown
Member

✈️ Pre-flight checklist

  • I have read the Contributing Guidelines on pull requests.
  • If this is a code change: I have written unit tests and/or added dogfooding pages to fully verify the new behavior.
  • If this is a new API or substantial change: the PR has an accompanying issue (closes #0000) and the maintainers have approved on my working plan.

📝 Summary

On Python 3.15, Pact breaks for users who turn on PEP 810 lazy imports globally with PYTHON_LAZY_IMPORTS=all or -X lazy_imports=all. Three attributes resolve to the wrong value:

Attribute Under lazy_imports=all Cause
pact_ffi.ffi the pact_ffi.ffi extension module, so the first FFI call raises AttributeError: module 'pact_ffi.ffi' has no attribute 'NULL' The submodule shares the attribute's name
pact.__version__, pact_ffi.__version__ the __version__ submodule, whenever __version_tuple__ is read first The submodule shares the attribute's name
pact.match, pact.generate AttributeError Only bound as a side effect of pact.xml importing them

The name collisions. from pact_ffi.ffi import ffi binds ffi lazily. When the submodule loads later, the import system sets the package attribute ffi to 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__.py now imports match and generate explicitly. Without lazy imports, both were already imported, so this adds no cost.

The pact_ffi fix reaches users with the next pact-python-ffi release.

🔥 Motivation

PEP 810 makes global lazy mode a user choice, so pact should work under it.

🔨 Test Plan

  • tests/test_lazy_imports.py runs 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.
  • pytest itself cannot run under PYTHON_LAZY_IMPORTS=all: its lazily imported fnmatch raises ImportCycleError. Setting PACT_TEST_LAZY_IMPORTS=1 instead makes tests/conftest.py lazy-load only the imports issued by pact and pact_ffi modules. A new CI step runs the suite that way on Python 3.15, Linux only.
  • Locally on Python 3.15:
    • Normal mode: the core and FFI suites pass, 558 passed and 14 skipped.
    • Lazy-import mode (PACT_TEST_LAZY_IMPORTS=1): the suite passes, 536 passed and 14 skipped.
    • Lazy-import mode on main: the same run gives 146 failures in tests/test_pact.py alone, and the new regression tests fail 6 of 8.

🔗 Related issues/PRs

Merges independently of #1734; the two branches touch no common files.

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

codecov Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 68%. Comparing base (a225d82) to head (f0424d8).
✅ All tests successful. No failed tests found.

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           
Flag Coverage Δ
examples 52% <100%> (+<1%) ⬆️
tests 67% <100%> (+<1%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

@JP-Ellis
JP-Ellis merged commit 1755374 into main Oct 16, 2026
35 checks passed
@JP-Ellis
JP-Ellis deleted the fix/lazy-import-compat branch October 16, 2026 02:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant