Skip to content

fix: using strawberry.lazy(...) break circular refs - #169

Merged
gazorby merged 3 commits into
mainfrom
worktree-fix+163-lazytype-eager-resolution
Jun 9, 2026
Merged

fix: using strawberry.lazy(...) break circular refs#169
gazorby merged 3 commits into
mainfrom
worktree-fix+163-lazytype-eager-resolution

Conversation

@gazorby

@gazorby gazorby commented Jun 2, 2026

Copy link
Copy Markdown
Owner

Description

Types of Changes

  • Core
  • Bugfix
  • New feature
  • Enhancement/optimization
  • Documentation

Issues Fixed or Closed by This PR

Checklist

  • My code follows the code style of this project.
  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • I have read the CONTRIBUTING document.
  • I have added tests to cover my changes.
  • I have tested the changes and verified that they work and don't break anything (as well as I can manage).

Summary by CodeRabbit

  • New Features

    • More robust handling of circular type references (lazy and forward refs), including accurate resolution inside containers and unions so schema types remain stable across overrides.
  • Tests

    • Added extensive unit tests covering lazy, forward-ref, global-scope and union-override scenarios to validate circular-resolution and union handling.

@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@gazorby, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 58 minutes and 35 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: ca401efb-3acc-4971-9d07-4380183fb409

📥 Commits

Reviewing files that changed from the base of the PR and between 27f2744 and fe0f8ad.

⛔ Files ignored due to path filters (6)
  • tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[forwardref_circular_default_scope].gql is excluded by !**/__snapshots__/**
  • tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[forwardref_circular_global_scope].gql is excluded by !**/__snapshots__/**
  • tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[lazy_circular_default_scope].gql is excluded by !**/__snapshots__/**
  • tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[lazy_circular_global_scope].gql is excluded by !**/__snapshots__/**
  • tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[union_override_lazy].gql is excluded by !**/__snapshots__/**
  • tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[union_override_plain].gql is excluded by !**/__snapshots__/**
📒 Files selected for processing (22)
  • src/strawchemy/utils/registry.py
  • src/strawchemy/utils/strawberry.py
  • tests/unit/mapping/test_schemas.py
  • tests/unit/schemas/forwardref/__init__.py
  • tests/unit/schemas/forwardref/a.py
  • tests/unit/schemas/forwardref/b.py
  • tests/unit/schemas/forwardref/query.py
  • tests/unit/schemas/forwardref_global/__init__.py
  • tests/unit/schemas/forwardref_global/a.py
  • tests/unit/schemas/forwardref_global/b.py
  • tests/unit/schemas/forwardref_global/query.py
  • tests/unit/schemas/lazy/__init__.py
  • tests/unit/schemas/lazy/a.py
  • tests/unit/schemas/lazy/b.py
  • tests/unit/schemas/lazy/query.py
  • tests/unit/schemas/lazy_global/__init__.py
  • tests/unit/schemas/lazy_global/a.py
  • tests/unit/schemas/lazy_global/b.py
  • tests/unit/schemas/lazy_global/query.py
  • tests/unit/schemas/union_override_lazy.py
  • tests/unit/schemas/union_override_plain.py
  • tests/unit/test_registry.py
📝 Walkthrough

Walkthrough

Core utilities now defer Strawberry LazyType resolution during decoration and refactor registry reference replacement to correctly handle containers and union members. A suite of test schema modules was added covering lazy and forward-ref circular patterns (default/global scope) and union-override cases.

Changes

Lazy Type Resolution and Circular Reference Support

Layer / File(s) Summary
Core LazyType handling and registry refactor
src/strawchemy/utils/strawberry.py, src/strawchemy/utils/registry.py
Adds resolve_lazy: bool to strawberry_contained_types(), treats LazyType as a leaf when resolve_lazy=False, refactors _TypeReference to frozen/slots with recursive replacement logic for containers and union members (using identity-first + name/LazyType.type_name fallback), and renames _get_type_info_type_info wired into registration calls.
Lazy circular type tests (default scope)
tests/unit/schemas/lazy/a.py, tests/unit/schemas/lazy/b.py, tests/unit/schemas/lazy/query.py
Adds ANode/BNode with bidirectional strawberry.lazy() circular references and a Query exposing both.
Lazy circular type tests (global scope)
tests/unit/schemas/lazy_global/a.py, tests/unit/schemas/lazy_global/b.py, tests/unit/schemas/lazy_global/query.py
Adds schema-scoped ColorNode/FruitNode across modules using strawberry.lazy() and a Query exposing both.
Forward-ref circular type tests (default scope)
tests/unit/schemas/forwardref/a.py, tests/unit/schemas/forwardref/b.py, tests/unit/schemas/forwardref/query.py
Adds ANode/BNode using plain forward-reference strings (from __future__ import annotations) and a Query exposing both.
Forward-ref circular type tests (global scope)
tests/unit/schemas/forwardref_global/a.py, tests/unit/schemas/forwardref_global/b.py, tests/unit/schemas/forwardref_global/query.py
Adds schema-scoped ColorNode/FruitNode with plain forward refs across modules and a Query exposing both.
Union override test (lazy member)
tests/unit/schemas/union_override_lazy.py
Adds a union where one member is strawberry.lazy(...), and a ParentNode field explicitly typed to the union to validate override replacement targets only the matching union member.
Union override test (plain members)
tests/unit/schemas/union_override_plain.py
Adds a union composed of plain (non-lazy) members and a ParentNode field explicitly typed to that union to validate override behavior preserves the static union type.
Test parametrization
tests/unit/mapping/test_schemas.py
Adds new parametrized test entries covering the new query schema modules for snapshot validation.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰
I nibble through circular threads, a careful hop and peep,
Lazy names stay lazy now — no eager dive too deep.
Tests in rows and unions neat, the registry mends its seams,
Hooray! I bound through schema loops and tidy up your dreams!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title directly addresses the main change: preventing eager resolution of strawberry.lazy(...) that breaks circular references.
Linked Issues check ✅ Passed Changes successfully address both #163 (eager LazyType resolution crash) and #174 (override=True union member rewriting) by treating LazyType as a leaf and preserving union containers during override replacement.
Out of Scope Changes check ✅ Passed All code changes are directly scoped to fixing LazyType handling and union override issues; test files implement required test scenarios for lazy/forward-ref variants.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree-fix+163-lazytype-eager-resolution

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov

codecov Bot commented Jun 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.15686% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 92.26%. Comparing base (6fd5039) to head (fe0f8ad).
⚠️ Report is 6 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/strawchemy/utils/registry.py 95.55% 1 Missing and 1 partial ⚠️
src/strawchemy/utils/strawberry.py 66.66% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #169      +/-   ##
==========================================
+ Coverage   92.18%   92.26%   +0.07%     
==========================================
  Files          70       70              
  Lines        6170     6206      +36     
  Branches      816      825       +9     
==========================================
+ Hits         5688     5726      +38     
+ Misses        333      332       -1     
+ Partials      149      148       -1     

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

@adiberk

adiberk commented Jun 2, 2026

Copy link
Copy Markdown

Wow! @gazorby killing it!

Just curious - is there any value of me submitting PRs?
Do you want me to stick with issues?

@gazorby

gazorby commented Jun 3, 2026

Copy link
Copy Markdown
Owner Author

Of course if you can spend some more time to spawn PRs that would be greatly appreciated, But your issues are really valuable!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/strawchemy/utils/strawberry.py`:
- Around line 47-51: The function strawberry_contained_types currently resolves
LazyType instances correctly via LazyType.resolve_type(), but the reviewer
suggests making the resolve_lazy parameter keyword-only for clarity; update the
function signature for strawberry_contained_types to accept resolve_lazy as a
keyword-only argument (use a bare * before resolve_lazy) while keeping the
default True and preserve the existing LazyType resolution logic that calls
type_.resolve_type() when resolve_lazy is true.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 02c647c4-a2d8-4393-be35-548afd8b6eec

📥 Commits

Reviewing files that changed from the base of the PR and between aeb1acb and 942832c.

⛔ Files ignored due to path filters (4)
  • tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[forwardref_circular_default_scope].gql is excluded by !**/__snapshots__/**
  • tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[forwardref_circular_global_scope].gql is excluded by !**/__snapshots__/**
  • tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[lazy_circular_default_scope].gql is excluded by !**/__snapshots__/**
  • tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[lazy_circular_global_scope].gql is excluded by !**/__snapshots__/**
📒 Files selected for processing (19)
  • src/strawchemy/utils/registry.py
  • src/strawchemy/utils/strawberry.py
  • tests/unit/mapping/test_schemas.py
  • tests/unit/schemas/forwardref/__init__.py
  • tests/unit/schemas/forwardref/a.py
  • tests/unit/schemas/forwardref/b.py
  • tests/unit/schemas/forwardref/query.py
  • tests/unit/schemas/forwardref_global/__init__.py
  • tests/unit/schemas/forwardref_global/a.py
  • tests/unit/schemas/forwardref_global/b.py
  • tests/unit/schemas/forwardref_global/query.py
  • tests/unit/schemas/lazy/__init__.py
  • tests/unit/schemas/lazy/a.py
  • tests/unit/schemas/lazy/b.py
  • tests/unit/schemas/lazy/query.py
  • tests/unit/schemas/lazy_global/__init__.py
  • tests/unit/schemas/lazy_global/a.py
  • tests/unit/schemas/lazy_global/b.py
  • tests/unit/schemas/lazy_global/query.py

Comment thread src/strawchemy/utils/strawberry.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/strawchemy/utils/registry.py (1)

265-269: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

LazyType arguments can bypass reference tracking after lazy deferral

At Line 265, argument tracking is gated only by get_object_definition(...). With resolve_lazy=False, LazyType won’t satisfy that condition, so _update_references is skipped for lazy-only argument types, and override/forward-ref updates won’t run for those arguments.

Suggested fix
         for field in object_definition.fields:
             for argument in field.arguments:
                 if any(
-                    get_object_definition(inner_type) is not None
+                    isinstance(inner_type, LazyType) or get_object_definition(inner_type) is not None
                     for inner_type in strawberry_contained_types(argument.type, resolve_lazy=False)
                 ):
                     self._update_references(argument, "input")
             self._update_references(field, graphql_type)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/strawchemy/utils/registry.py` around lines 265 - 269, The argument
reference-tracking skips LazyType-only arguments because get_object_definition
is called with resolve_lazy=False; change the gating so lazy deferred types are
resolved or explicitly detected before calling self._update_references. Locate
the expression using get_object_definition(...) inside the generator over
strawberry_contained_types(argument.type, resolve_lazy=False) and either (a)
call strawberry_contained_types with resolve_lazy=True or (b) add an additional
check for lazy types (e.g., detect LazyType or use the library's is_lazy helper)
and treat them as matchable so that self._update_references(argument, "input")
runs for lazy arguments as well.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/strawchemy/utils/registry.py`:
- Around line 265-269: The argument reference-tracking skips LazyType-only
arguments because get_object_definition is called with resolve_lazy=False;
change the gating so lazy deferred types are resolved or explicitly detected
before calling self._update_references. Locate the expression using
get_object_definition(...) inside the generator over
strawberry_contained_types(argument.type, resolve_lazy=False) and either (a)
call strawberry_contained_types with resolve_lazy=True or (b) add an additional
check for lazy types (e.g., detect LazyType or use the library's is_lazy helper)
and treat them as matchable so that self._update_references(argument, "input")
runs for lazy arguments as well.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 99958f68-fddf-4989-807a-caa954b7a888

📥 Commits

Reviewing files that changed from the base of the PR and between 942832c and 45f1ef6.

⛔ Files ignored due to path filters (2)
  • tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[union_override_lazy].gql is excluded by !**/__snapshots__/**
  • tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[union_override_plain].gql is excluded by !**/__snapshots__/**
📒 Files selected for processing (4)
  • src/strawchemy/utils/registry.py
  • tests/unit/mapping/test_schemas.py
  • tests/unit/schemas/union_override_lazy.py
  • tests/unit/schemas/union_override_plain.py

@gazorby
gazorby force-pushed the worktree-fix+163-lazytype-eager-resolution branch from 45f1ef6 to 27f2744 Compare June 7, 2026 09:22

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/unit/schemas/union_override_lazy.py`:
- Line 11: The import statement includes Union which is no longer needed when
using PEP 604 syntax. Remove Union from the import statement on the first line.
Then locate the ChildUnion type definition (referenced in the comment as also
applying to lines 26-29) and replace the Union syntax with the pipe operator
syntax, changing from Union[GroupNode, Annotated[...]] to GroupNode |
Annotated[...] to satisfy the UP007 linting rule.

In `@tests/unit/schemas/union_override_plain.py`:
- Line 10: Replace the use of typing.Union for the ChildUnion type with Python
3.10+ union operator; change any occurrences of Union[GroupNode, TagNode] to
GroupNode | TagNode and remove the now-unused Union import from the top of
tests/unit/schemas/union_override_plain.py (also update the other occurrences
mentioned around lines 30–33 where ChildUnion is declared or referenced).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: cfeeaeff-5a2e-4364-944b-82d586582278

📥 Commits

Reviewing files that changed from the base of the PR and between 45f1ef6 and 27f2744.

⛔ Files ignored due to path filters (6)
  • tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[forwardref_circular_default_scope].gql is excluded by !**/__snapshots__/**
  • tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[forwardref_circular_global_scope].gql is excluded by !**/__snapshots__/**
  • tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[lazy_circular_default_scope].gql is excluded by !**/__snapshots__/**
  • tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[lazy_circular_global_scope].gql is excluded by !**/__snapshots__/**
  • tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[union_override_lazy].gql is excluded by !**/__snapshots__/**
  • tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[union_override_plain].gql is excluded by !**/__snapshots__/**
📒 Files selected for processing (21)
  • src/strawchemy/utils/registry.py
  • src/strawchemy/utils/strawberry.py
  • tests/unit/mapping/test_schemas.py
  • tests/unit/schemas/forwardref/__init__.py
  • tests/unit/schemas/forwardref/a.py
  • tests/unit/schemas/forwardref/b.py
  • tests/unit/schemas/forwardref/query.py
  • tests/unit/schemas/forwardref_global/__init__.py
  • tests/unit/schemas/forwardref_global/a.py
  • tests/unit/schemas/forwardref_global/b.py
  • tests/unit/schemas/forwardref_global/query.py
  • tests/unit/schemas/lazy/__init__.py
  • tests/unit/schemas/lazy/a.py
  • tests/unit/schemas/lazy/b.py
  • tests/unit/schemas/lazy/query.py
  • tests/unit/schemas/lazy_global/__init__.py
  • tests/unit/schemas/lazy_global/a.py
  • tests/unit/schemas/lazy_global/b.py
  • tests/unit/schemas/lazy_global/query.py
  • tests/unit/schemas/union_override_lazy.py
  • tests/unit/schemas/union_override_plain.py


from __future__ import annotations

from typing import Annotated, Union

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Prefer | syntax for ChildUnion to satisfy UP007.

Use PEP 604 union syntax here (GroupNode | Annotated[...]) and remove Union from imports; this keeps the test module lint-clean.

Suggested diff
-from typing import Annotated, Union
+from typing import Annotated
@@
 ChildUnion = Annotated[
-    Union[GroupNode, Annotated["TagNode", strawberry.lazy("tests.unit.schemas.union_override_lazy")]],
+    GroupNode | Annotated["TagNode", strawberry.lazy("tests.unit.schemas.union_override_lazy")],
     strawberry.union("ChildUnion"),
 ]

Also applies to: 26-29

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/schemas/union_override_lazy.py` at line 11, The import statement
includes Union which is no longer needed when using PEP 604 syntax. Remove Union
from the import statement on the first line. Then locate the ChildUnion type
definition (referenced in the comment as also applying to lines 26-29) and
replace the Union syntax with the pipe operator syntax, changing from
Union[GroupNode, Annotated[...]] to GroupNode | Annotated[...] to satisfy the
UP007 linting rule.

Source: Linters/SAST tools


from __future__ import annotations

from typing import Annotated, Union

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Use | unions for ChildUnion (UP007).

Switch Union[GroupNode, TagNode] to GroupNode | TagNode and drop the Union import to avoid lint churn.

Suggested diff
-from typing import Annotated, Union
+from typing import Annotated
@@
 ChildUnion = Annotated[
-    Union[GroupNode, TagNode],
+    GroupNode | TagNode,
     strawberry.union("ChildUnion"),
 ]

Also applies to: 30-33

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/schemas/union_override_plain.py` at line 10, Replace the use of
typing.Union for the ChildUnion type with Python 3.10+ union operator; change
any occurrences of Union[GroupNode, TagNode] to GroupNode | TagNode and remove
the now-unused Union import from the top of
tests/unit/schemas/union_override_plain.py (also update the other occurrences
mentioned around lines 30–33 where ChildUnion is declared or referenced).

Source: Linters/SAST tools

@gazorby
gazorby force-pushed the worktree-fix+163-lazytype-eager-resolution branch from 27f2744 to b4583e3 Compare June 8, 2026 06:28
@gazorby
gazorby force-pushed the worktree-fix+163-lazytype-eager-resolution branch from 27d2959 to fe0f8ad Compare June 9, 2026 18:55
@gazorby
gazorby merged commit a3aecc2 into main Jun 9, 2026
95 of 96 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants