fix: resolve the models/Rekor-client import cycle instead of lazy imports (#1536) by oaksprout · Pull Request #1845 · sigstore/sigstore-python · GitHub
Skip to content

fix: resolve the models/Rekor-client import cycle instead of lazy imports (#1536) - #1845

Closed
oaksprout wants to merge 1 commit into
sigstore:mainfrom
oaksprout:fix/models-lazy-import-1536
Closed

oaksprout wants to merge 1 commit into
sigstore:mainfrom
oaksprout:fix/models-lazy-import-1536

Conversation

@oaksprout

Copy link
Copy Markdown

You set the work. #1536: the lazy imports papering over the circular dependency between models.py and the Rekor clients should be cleaned up.

It's done. The cycle is resolved rather than relocated — which is where the earlier attempt (#1667) stalled, per your review of it: TransparencyLogEntry moves to a leaf module (_internal/rekor/entry.py), the follow-up remedy named in that thread. InvalidBundle/IncompatibleEntry join the rest of the exception hierarchy in errors.py; all lazy/TYPE_CHECKING tricks in the affected import paths become plain module-level imports; sigstore.models re-exports the moved names via explicit __all__ so the public API is unchanged.

Here's the evidence.

  • Fresh clone at 181074f4, patch applied, uv sync --locked, then the network was disconnected.
  • Offline unit suite in a clean python:3.12-bookworm container: 175 passed / 37 skipped, matching base counts plus 9 new import-cycle tests (4 of which fail on pre-fix code, verified by stash).
  • make lint clean solve-side: ruff, mypy strict (31 files), bandit, api-docs check.
  • mkdocs built before and after and diffed: identical documented members — the __all__ re-export exists partly because this check caught a doc regression during development.
Audit trail — an independently checkable record that these checks ran, in this order, before this PR existed
Base commit 181074f4dc11b7e85ef44556e25248ef14fcb554
Container python:3.12-bookworm@sha256:9bed8554… (linux/arm64), network off during tests
The patch, content-addressed record
The verification result, signed by a second key record
Timestamped sequence work assigned → work delivered → checks passed

What this proves: the checks ran, in that order, on exactly this patch, before this PR was opened — none of it can be backdated or swapped afterwards. What it doesn't prove: that the fix is right. The two signing keys are distinct but run by the same project, and the record lives on a test network. Correctness is your judgement, which is the point.

Written by an AI agent; reviewed and sent by a human who answers the review. We're testing whether work checked this way is useful to maintainers — blunt feedback welcome, including "don't".


Everything below is written by Claude

Fixes #1536.

New leaf module sigstore/_internal/rekor/entry.py holds TransparencyLogEntry verbatim — it depends only on merkle, rekor/checkpoint, trust (TYPE_CHECKING), _utils, and errors, never on sigstore.models or the clients. client.py/client_v2.py import it directly at module level; models.py imports RekorClient/RekorV2Client/TransparencyLogEntry at module scope and SigningConfig.get_tlogs() loses its local imports. Three TYPE_CHECKING-only importers updated to reference the leaf module. New test/unit/test_imports.py imports each formerly-cyclic module first in fresh subprocess interpreters and asserts the re-exports are the same objects.

Notes for the reviewer

  • InvalidBundle/IncompatibleEntry now render on both docs/api/errors.md (definition) and docs/api/models.md (re-export) — harmless duplication; happy to suppress one if you'd rather.
  • The online/integration suite wasn't run (network-gated); offline suite and full lint were.

sigstore#1496 moved `ClientTrustConfig`/`SigningConfig`/`TrustedRoot` into
`sigstore/models.py`. `SigningConfig.get_tlogs()` needs `RekorClient`
and `RekorV2Client`, and those clients need `TransparencyLogEntry` to
type their responses -- a genuine circular import at the module level,
worked around with a function-local (lazy) import in `get_tlogs()`.

sigstore#1667 tried to fix this by moving the lazy import to the other side of
the cycle (Rekor clients importing `TransparencyLogEntry` lazily
instead), but a maintainer noted it was still just a lazy import in a
different spot, not a real fix.

This resolves the cycle instead of relocating it, by moving the shared
type out of `models.py` into a leaf module:

* `TransparencyLogEntry` now lives in `sigstore/_internal/rekor/entry.py`,
  which only depends on Rekor/merkle/trust internals -- never on
  `sigstore.models` or the Rekor clients. Both `models.py` (via
  `SigningConfig.get_tlogs()`) and the Rekor clients (`client.py`,
  `client_v2.py`) now import it directly, at module scope, with no lazy
  imports anywhere in the graph.
* `InvalidBundle`/`IncompatibleEntry` move to `sigstore/errors.py`
  (alongside the rest of the exception hierarchy), since
  `TransparencyLogEntry` raises them and `errors.py` has no dependency
  on `models.py` either.
* `sigstore.models` re-exports all three via `__all__` for backwards
  compatibility (`from sigstore.models import TransparencyLogEntry` /
  `InvalidBundle` keep working) and so they keep their generated
  API-reference entries under `docs/api/models.md`.

Adds `test/unit/test_imports.py`, which imports every module that used
to sit on the cycle first, in a fresh interpreter, to guard against a
regression, and checks that `SigningConfig.get_tlogs()` no longer needs
a lazy import.

No behavioral or public API change: `sigstore.models.TransparencyLogEntry`,
`InvalidBundle`, and `IncompatibleEntry` are unchanged in name, contents,
and import path.

Fixes sigstore#1536

Signed-off-by: Oaksprout <oaksproutthetan@gmail.com>
@woodruffw

Copy link
Copy Markdown
Member

@woodruffw woodruffw closed this Jul 28, 2026
@sigstore sigstore locked and limited conversation to collaborators Jul 28, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix lazy import in models.py

2 participants