Skip to content

Conversation

@rcybulski1122012
Copy link
Member

@rcybulski1122012 rcybulski1122012 commented Oct 9, 2025

Description

Adds support for lazy unions by adjusting GraphQLCoreConverter.from_type

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 Sourcery

Add support for lazy unions by adjusting type conversion logic to unwrap Annotated types, include tests for both eager and lazy union members, and update release notes

Bug Fixes:

  • Handle Annotated types for lazy unions in GraphQLCoreConverter.from_type

Enhancements:

  • Add support for lazy unions in schema definitions via Annotated types

Documentation:

  • Add release note for patch release

Tests:

  • Add tests for lazy unions with non-lazy and lazy union members

@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Oct 9, 2025

Reviewer's Guide

Unwraps Annotated types in GraphQLCoreConverter to support lazy union definitions, accompanied by new tests and a release note update.

File-Level Changes

Change Details Files
Handle lazy unions by unwrapping Annotated types in from_type
  • Added a guard checking if typing.get_origin(type_) is Annotated and has two args
  • Reassign type_ to the second argument of Annotated before further processing
strawberry/schema/schema_converter.py
Introduce tests for lazy unions
  • Created tests covering unions with non-lazy and lazy members
  • Use print_schema to assert correct SDL output for both scenarios
tests/schema/test_lazy_types/test_lazy_unions.py
Add a patch note for lazy unions support
  • Included a new entry in RELEASE.md specifying the patch release type
RELEASE.md

Possibly linked issues

  • Do the Lazy-Types support Union? #3381: The PR adds support for lazy unions by adjusting GraphQLCoreConverter.from_type to correctly process Annotated types, resolving the TypeError described in the issue.
  • #UserProvidedIssue: The PR adds support for lazy unions by modifying GraphQLCoreConverter.from_type to correctly handle Annotated types with Union, resolving the TypeError described in the issue.
  • Do the Lazy-Types support Union? #3381: The PR fixes the issue by adding support for lazy unions, specifically by adjusting GraphQLCoreConverter.from_type to correctly handle Annotated types used for lazy unions, which was causing the schema generation failure in the issue.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey there - I've reviewed your changes and they look great!

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location> `tests/schema/test_lazy_types/test_lazy_unions.py:26-52` </location>
<code_context>
+], strawberry.union("LazyABUnion", types=[TypeALazy, TypeBLazy])]
+
+
+def test_lazy_union_with_non_lazy_members():
+    @strawberry.type
+    class Query:
+        ab: Annotated["ABUnion", strawberry.lazy("tests.schema.test_lazy_types.test_lazy_unions")]
+
+    expected = """
+        union ABUnion = TypeA | TypeB
+
+        type Query {
+          ab: ABUnion!
+        }
+
+        type TypeA {
+          a: Int!
+        }
+
+        type TypeB {
+          b: Int!
+        }
+    """
+
+    schema = strawberry.Schema(query=Query)
+    assert print_schema(schema) == textwrap.dedent(expected).strip()
+
+
</code_context>

<issue_to_address>
**suggestion (testing):** Missing tests for error conditions and invalid lazy union definitions.

Add tests for cases like referencing non-existent types, invalid module paths, and empty type lists to verify proper error handling in lazy unions.

```suggestion
+
+
+import pytest
+
+def test_lazy_union_with_non_lazy_members():
+    @strawberry.type
+    class Query:
+        ab: Annotated["ABUnion", strawberry.lazy("tests.schema.test_lazy_types.test_lazy_unions")]
+
+    expected = """
+        union ABUnion = TypeA | TypeB
+
+        type Query {
+          ab: ABUnion!
+        }
+
+        type TypeA {
+          a: Int!
+        }
+
+        type TypeB {
+          b: Int!
+        }
+    """
+
+    schema = strawberry.Schema(query=Query)
+    assert print_schema(schema) == textwrap.dedent(expected).strip()
+
+
+# Error condition: referencing non-existent type in lazy union
+def test_lazy_union_with_nonexistent_type():
+    NonExistentTypeLazy = Annotated["NonExistentType", strawberry.lazy("tests.schema.test_lazy_types.test_lazy_unions")]
+    with pytest.raises(ModuleNotFoundError):
+        strawberry.union("BadUnion", types=[NonExistentTypeLazy])
+
+
+# Error condition: invalid module path in lazy annotation
+def test_lazy_union_with_invalid_module_path():
+    InvalidTypeLazy = Annotated["TypeA", strawberry.lazy("invalid.module.path")]
+    with pytest.raises(ModuleNotFoundError):
+        strawberry.union("InvalidUnion", types=[InvalidTypeLazy])
+
+
+# Error condition: empty type list in lazy union
+def test_lazy_union_with_empty_type_list():
+    with pytest.raises(ValueError):
+        strawberry.union("EmptyUnion", types=[])
+
```
</issue_to_address>

### Comment 2
<location> `tests/schema/test_lazy_types/test_lazy_unions.py:53-50` </location>
<code_context>
+    assert print_schema(schema) == textwrap.dedent(expected).strip()
+
+
+def test_lazy_union_with_lazy_members():
+    @strawberry.type
+    class Query:
+        ab: Annotated["LazyABUnion", strawberry.lazy("tests.schema.test_lazy_types.test_lazy_unions")]
+
+    expected = """
+           union LazyABUnion = TypeA | TypeB
+
+           type Query {
+             ab: LazyABUnion!
+           }
+
+           type TypeA {
+             a: Int!
+           }
+
+           type TypeB {
+             b: Int!
+           }
+       """
+
+    schema = strawberry.Schema(query=Query)
+    assert print_schema(schema) == textwrap.dedent(expected).strip()
</code_context>

<issue_to_address>
**suggestion (testing):** Tests only verify schema printing, not query execution or runtime type resolution.

Consider adding tests that execute queries to ensure lazy unions resolve correctly at runtime, verifying expected results for each member type.

Suggested implementation:

```python
def test_lazy_union_with_lazy_members():
    import strawberry
    from typing import Annotated, Union
    import textwrap

    @strawberry.type
    class TypeA:
        a: int

    @strawberry.type
    class TypeB:
        b: int

    LazyABUnion = strawberry.union(
        "LazyABUnion",
        types=(TypeA, TypeB),
    )

    @strawberry.type
    class Query:
        ab: Annotated[LazyABUnion, strawberry.lazy("tests.schema.test_lazy_types.test_lazy_unions")]

        @strawberry.field
        def ab_type_a(self) -> LazyABUnion:
            return TypeA(a=123)

        @strawberry.field
        def ab_type_b(self) -> LazyABUnion:
            return TypeB(b=456)

    expected = """
           union LazyABUnion = TypeA | TypeB

```

```python
    schema = strawberry.Schema(query=Query)
    assert print_schema(schema) == textwrap.dedent(expected).strip()

    # Runtime query execution tests
    from strawberry.test import GraphQLTestClient

    client = GraphQLTestClient(schema)

    # Test TypeA resolution
    response_a = client.query(
        """
        {
            abTypeA {
                ... on TypeA {
                    a
                }
                ... on TypeB {
                    b
                }
            }
        }
        """
    )
    assert response_a.data == {"abTypeA": {"a": 123, "b": None}}

    # Test TypeB resolution
    response_b = client.query(
        """
        {
            abTypeB {
                ... on TypeA {
                    a
                }
                ... on TypeB {
                    b
                }
            }
        }
        """
    )
    assert response_b.data == {"abTypeB": {"a": None, "b": 456}}

```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@botberry
Copy link
Member

botberry commented Oct 9, 2025

Thanks for adding the RELEASE.md file!

Here's a preview of the changelog:


Adds support for lazy unions.

Here's the tweet text:

🆕 Release (next) is out! Thanks to Radosław Cybulski for the PR 👏

Get it here 👉 https://strawberry.rocks/release/(next)

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

Greptile Overview

Summary

This PR adds support for lazy unions in Strawberry GraphQL by modifying the `GraphQLCoreConverter.from_type` method to handle `Annotated` types containing union metadata. The change addresses issue #3381 where users encountered TypeErrors when attempting to use lazy types with unions.

The core implementation involves a simple but effective check that detects Annotated types with exactly 2 arguments and extracts the union definition from the second argument. This allows the existing union handling logic to process lazy unions of the form Annotated[Union[...], strawberry.union(...)] without requiring extensive changes to the union resolution system.

The fix integrates seamlessly with Strawberry's existing lazy type infrastructure, which already uses Annotated types to wrap lazy references with strawberry.lazy() metadata. By extending this pattern to unions, developers can now define lazy references to union types just like they can with regular types, maintaining consistency across the type system.

Comprehensive test coverage has been added that validates both lazy unions with non-lazy members and lazy unions with lazy members, ensuring the implementation works correctly across different lazy loading scenarios.

Important Files Changed

Changed Files
Filename Score Overview
strawberry/schema/schema_converter.py 4/5 Added 3-line check to extract union definitions from Annotated types, enabling lazy union support
tests/schema/test_lazy_types/test_lazy_unions.py 5/5 Added comprehensive test coverage for lazy unions with both lazy and non-lazy members
RELEASE.md 5/5 Added release notes indicating patch-level support for lazy unions

Confidence score: 4/5

  • This PR is safe to merge with minimal risk as it addresses a specific bug with a focused solution
  • Score reflects the targeted nature of the fix and comprehensive test coverage, though the implementation could benefit from additional edge case handling
  • Pay close attention to strawberry/schema/schema_converter.py to ensure the type extraction logic handles all Annotated union patterns correctly

Sequence Diagram

sequenceDiagram
    participant User
    participant Schema as "strawberry.Schema"
    participant Converter as "GraphQLCoreConverter"
    participant LazyType as "LazyType"
    participant Union as "StrawberryUnion"

    User->>Schema: "Create schema with lazy union field"
    Schema->>Converter: "from_type(lazy_union_field)"
    
    Note over Converter: Check if type is Annotated with 2 args
    Converter->>Converter: "typing.get_origin(type_) is Annotated?"
    Converter->>Converter: "len(typing.get_args(type_)) == 2?"
    Converter->>Converter: "Extract actual type from args[1]"
    
    alt Type is StrawberryUnion
        Converter->>Union: "from_union(union_type)"
        Union->>Union: "Validate union member types"
        
        loop For each union member type
            Union->>Converter: "from_type(member_type)"
            
            alt Member is LazyType
                Converter->>LazyType: "from_type(lazy_type)"
                LazyType->>LazyType: "resolve_type()"
                LazyType-->>Converter: "resolved_type"
                Converter->>Converter: "from_type(resolved_type)"
            else Member is regular type
                Converter->>Converter: "Process regular type"
            end
            
            Converter-->>Union: "GraphQL type"
        end
        
        Union->>Union: "Create GraphQLUnionType"
        Union-->>Converter: "GraphQLUnionType"
    end
    
    Converter-->>Schema: "GraphQL type"
    Schema-->>User: "Compiled schema"
Loading

3 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Comment on lines 866 to 869
# to handle lazy unions
if typing.get_origin(type_) is Annotated and len(typing.get_args(type_)) == 2:
type_ = typing.get_args(type_)[1]

Copy link
Member Author

Choose a reason for hiding this comment

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

It isn't the best place for this code. It would be better if resolving a lazy type would return a StrawberryUnion object in this case, but I couldn't make it work 🤔

Copy link
Member

Choose a reason for hiding this comment

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

could be worth trying in another PR!

rcybulski1122012 and others added 4 commits October 9, 2025 08:42
@codecov
Copy link

codecov bot commented Oct 9, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 94.29%. Comparing base (74c2cef) to head (4dbe410).
⚠️ Report is 54 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4017      +/-   ##
==========================================
- Coverage   94.75%   94.29%   -0.47%     
==========================================
  Files         520      533      +13     
  Lines       33902    34893     +991     
  Branches     1754     1846      +92     
==========================================
+ Hits        32123    32901     +778     
- Misses       1497     1687     +190     
- Partials      282      305      +23     
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@codspeed-hq
Copy link

codspeed-hq bot commented Oct 9, 2025

CodSpeed Performance Report

Merging #4017 will not alter performance

Comparing rcybulski1122012:handle-lazy-unions (4dbe410) with main (53bf569)

Summary

✅ 26 untouched

TypeALazy,
TypeBLazy,
],
strawberry.union("LazyABUnion", types=[TypeALazy, TypeBLazy]),
Copy link
Member Author

Choose a reason for hiding this comment

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

If a union does not define types, an error will be raised that union has to have at least one member

Copy link
Member

@patrick91 patrick91 left a comment

Choose a reason for hiding this comment

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

Thank you so much!

Comment on lines 866 to 869
# to handle lazy unions
if typing.get_origin(type_) is Annotated and len(typing.get_args(type_)) == 2:
type_ = typing.get_args(type_)[1]

Copy link
Member

Choose a reason for hiding this comment

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

could be worth trying in another PR!

@patrick91 patrick91 merged commit 11a9b21 into strawberry-graphql:main Oct 10, 2025
89 of 90 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

Development

Successfully merging this pull request may close these issues.

3 participants