Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,12 @@ jobs:
working-directory: pact-python-ffi
run: hatch run test.py${{ matrix.python-version }}:test --junit-xml=junit.xml --cov-report=xml:coverage-ffi.xml

- name: Run tests (lazy imports)
if: matrix.python-version == '3.15' && matrix.os == 'ubuntu-latest'
env:
PACT_TEST_LAZY_IMPORTS: '1'
run: hatch run test.py${{ matrix.python-version }}:test --no-cov

- name: Upload coverage
if: matrix.python-version == env.STABLE_PYTHON_VERSION && matrix.os == 'ubuntu-latest'
uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
Expand Down
14 changes: 11 additions & 3 deletions pact-python-ffi/src/pact_ffi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,17 @@
from enum import Enum
from typing import TYPE_CHECKING, Any, Literal

from pact_ffi.__version__ import __version__ as __version__
from pact_ffi.__version__ import __version_tuple__ as __version_tuple__
from pact_ffi.ffi import ffi, lib # type: ignore[import]
# The `__version__` and `ffi` submodules share their names with attributes of
# this package. Under PEP 810 lazy imports, a submodule loaded after its
# attribute is bound replaces that attribute. Assigning from an aliased module
# binds the attribute after the submodule has loaded.
import pact_ffi.__version__ as _version_module
import pact_ffi.ffi as _ffi_module # type: ignore[import]

__version__ = _version_module.__version__
__version_tuple__ = _version_module.__version_tuple__
ffi = _ffi_module.ffi
lib = _ffi_module.lib

if TYPE_CHECKING:
import datetime as dt
Expand Down
11 changes: 10 additions & 1 deletion src/pact/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,11 +111,20 @@

from __future__ import annotations

# The `__version__` submodule shares its name with the attribute. Under PEP 810
# lazy imports, a submodule loaded after its attribute is bound replaces that
# attribute. Assigning from an aliased module binds the attribute after the
# submodule has loaded.
import pact.__version__ as _version_module
from pact import generate as generate
from pact import match as match
from pact import xml as xml
from pact.__version__ import __version__, __version_tuple__
from pact.pact import Pact
from pact.verifier import Verifier

__version__ = _version_module.__version__
__version_tuple__ = _version_module.__version_tuple__

__url__ = "https://github.com/pact-foundation/pact-python"
__license__ = "MIT"
__author__ = "Pact Foundation"
Expand Down
12 changes: 12 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,21 @@
from __future__ import annotations

import json
import os
import sys
import tempfile
from pathlib import Path

if sys.version_info >= (3, 15) and os.environ.get("PACT_TEST_LAZY_IMPORTS"):
# Approximates a user running with `PYTHON_LAZY_IMPORTS=all`, which pytest
# itself does not support: only imports made by Pact's modules are lazy.
sys.set_lazy_imports("all")
sys.set_lazy_imports_filter(
lambda importer, _name, _fromlist: (
importer is not None and importer.split(".")[0] in {"pact", "pact_ffi"}
)
)

import pytest

import pact_ffi
Expand Down
56 changes: 56 additions & 0 deletions tests/test_lazy_imports.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"""
Tests for compatibility with PEP 810 lazy imports.
"""

from __future__ import annotations

import subprocess
import sys

import pytest

pytestmark = pytest.mark.skipif(
sys.version_info < (3, 15),
reason="Lazy imports require Python 3.15",
)


@pytest.mark.parametrize(
("expression", "expected"),
[
pytest.param("pact_ffi.ffi", "FFI", id="pact_ffi.ffi"),
pytest.param("pact_ffi.lib", "Lib", id="pact_ffi.lib"),
pytest.param("pact_ffi.__version__", "str", id="pact_ffi.__version__"),
pytest.param("pact.__version__", "str", id="pact.__version__"),
pytest.param("pact.match", "module", id="pact.match"),
pytest.param("pact.generate", "module", id="pact.generate"),
pytest.param("pact.xml", "module", id="pact.xml"),
],
)
def test_attribute_under_lazy_imports(expression: str, expected: str) -> None:
# Reading `__version_tuple__` first loads the `__version__` submodule
# before `__version__` itself is resolved, which is the order that once
# replaced the attribute with the submodule.
code = (
"import pact, pact_ffi; "
"pact.__version_tuple__; pact_ffi.__version_tuple__; "
f"print(type({expression}).__name__)"
)
result = subprocess.run( # noqa: S603
[sys.executable, "-X", "lazy_imports=all", "-c", code],
capture_output=True,
text=True,
check=True,
)
assert result.stdout.strip() == expected


def test_ffi_call_under_lazy_imports() -> None:
code = "import pact_ffi; print(pact_ffi.version())"
result = subprocess.run( # noqa: S603
[sys.executable, "-X", "lazy_imports=all", "-c", code],
capture_output=True,
text=True,
check=True,
)
assert result.stdout.strip()
Loading