Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add support for including pydantic computed fields #3798

Open
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

tylernisonoff
Copy link

@tylernisonoff tylernisonoff commented Mar 4, 2025

Adds the ability to include pydantic computed fields when using pydantic.type decorator.

Description

Fixes #3607
Reserves backwards compatibility by only enabling this behavior with a bool flag.

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

Adds support for including Pydantic computed fields in Strawberry types when using the strawberry.experimental.pydantic.type decorator. This feature is enabled via the include_computed flag.

New Features:

  • Adds the ability to include pydantic computed fields when using pydantic.type decorator by introducing a new include_computed flag.

Tests:

  • Adds a test case to verify that computed fields are correctly included in the generated GraphQL schema when the include_computed flag is enabled.

Copy link
Contributor

sourcery-ai bot commented Mar 4, 2025

Reviewer's Guide by Sourcery

This pull request introduces support for including Pydantic computed fields in Strawberry types generated using the strawberry.experimental.pydantic.type decorator. A new include_computed parameter has been added to the decorator, which, when set to True, will include computed fields from the Pydantic model in the generated Strawberry type. The PydanticCompat class has been updated to fetch computed fields, and the ensure_all_auto_fields_in_pydantic function has been modified to account for computed fields when validating auto fields.

Sequence diagram for including computed fields

sequenceDiagram
    participant User
    participant strawberry.type
    participant PydanticCompat
    participant PydanticModel

    User->>strawberry.type: Call type(model, include_computed=True)
    strawberry.type->>PydanticCompat: from_model(model)
    strawberry.type->>PydanticCompat: get_model_fields(model, include_computed=True)
    PydanticCompat->>PydanticModel: model.model_fields
    PydanticCompat->>PydanticModel: model.model_computed_fields
    PydanticCompat-->>strawberry.type: model_fields
    strawberry.type-->>User: StrawberryType
Loading

File-Level Changes

Change Details Files
Added the ability to include Pydantic computed fields in Strawberry types.
  • Introduced a new include_computed parameter to the strawberry.experimental.pydantic.type decorator.
  • Modified PydanticCompat to fetch computed fields from Pydantic models.
  • Updated ensure_all_auto_fields_in_pydantic to consider computed fields when include_computed is True.
strawberry/experimental/pydantic/_compat.py
strawberry/experimental/pydantic/utils.py
strawberry/experimental/pydantic/object_type.py
tests/experimental/pydantic/schema/test_computed.py

Possibly linked issues


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!
  • Generate a plan of action for an issue: Comment @sourcery-ai plan on
    an issue to generate a plan of action for it.

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 @tylernisonoff - I've reviewed your changes - here's some feedback:

Overall Comments:

  • Consider adding a test case that uses a computed field with a different name than the pydantic field.
Here's what I looked at during the review
  • 🟢 General issues: all looks good
  • 🟢 Security: all looks good
  • 🟡 Testing: 1 issue found
  • 🟡 Complexity: 1 issue found
  • 🟢 Documentation: all looks good

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.

Comment on lines +60 to +63
result = schema.execute_sync(query)
assert not result.errors
assert result.data["user"]["age"] == 1
assert result.data["user"]["nextAge"] == 2
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion (testing): Add test verifying the behavior when include_computed is False

It's important to explicitly test the negative case where include_computed=False. Verify that the computed field is not included in the schema and is not resolvable.

Suggested implementation:

    assert result.data["user"]["age"] == 1
    assert result.data["user"]["nextAge"] == 2

    # New test for behavior when include_computed is False
    def test_computed_not_included_when_disabled():
        # Assuming build_schema accepts an include_computed flag.
        schema = build_schema(include_computed=False)

        expected_schema = """
        type UserNoComputed {
          age: Int!
        }
        """
        # Check that the printed schema does not include computed fields.
        assert str(schema) == textwrap.dedent(expected_schema).strip()

        query = "{ user { age nextAge } }"

        # Execute a query that attempts to fetch the computed field.
        result = schema.execute_sync(query)

        # We expect an error since "nextAge" should not be resolvable.
        assert result.errors
        # Optionally, verify the error message indicates nextAge is not a valid field.
        assert "nextAge" in result.errors[0].message

Note:

  1. Ensure that the function build_schema exists and accepts the include_computed flag.
  2. Adjust the error message check as needed to match the actual error output from your GraphQL library.
  3. If you prefer, you could wrap the new test in a dedicated test class or module that groups computed field tests.

)
return new_fields

def get_model_fields(
Copy link
Contributor

Choose a reason for hiding this comment

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

issue (complexity): Consider extracting the shared field conversion logic into a helper function with a flag to handle computed fields differently, and then call this helper from both get_model_fields and get_model_computed_fields functions.

Consider extracting the shared conversion logic into a single helper that toggles computed behavior via a flag. This reduces duplication while keeping both functionalities intact. For example:

def _convert_field(self, name: str, field: Any, *, computed: bool = False) -> CompatModelField:
    type_val = field.return_type if computed else field.annotation
    return CompatModelField(
        name=name,
        type_=type_val,
        outer_type_=type_val,
        default=None if computed else field.default,
        default_factory=None if computed else field.default_factory,  # type: ignore
        required=False if computed else field.is_required(),
        alias=field.alias,
        allow_none=False,
        has_alias=field is not None,
        description=field.description,
        _missing_type=self.PYDANTIC_MISSING_TYPE,
        is_v1=False,
    )

Then update your methods as follows:

def get_model_fields(self, model: type[BaseModel], include_computed: bool = False
) -> dict[str, CompatModelField]:
    basic_fields = {
        name: self._convert_field(name, field)
        for name, field in model.model_fields.items()
    }
    if include_computed:
        computed_fields = {
            name: self._convert_field(name, field, computed=True)
            for name, field in model.model_computed_fields.items()
        }
        basic_fields |= computed_fields
    return basic_fields

def get_model_computed_fields(self, model: type[BaseModel]) -> dict[str, CompatModelField]:
    return {
        name: self._convert_field(name, field, computed=True)
        for name, field in model.model_computed_fields.items()
    }

This approach maintains current functionality and reduces complexity by consolidating repeated logic.

@botberry
Copy link
Member

botberry commented Mar 4, 2025

Thanks for adding the RELEASE.md file!

Here's a preview of the changelog:


Adds the ability to include pydantic computed fields when using pydantic.type decorator.

Example:

class UserModel(pydantic.BaseModel):
    age: int

    @computed_field
    @property
    def next_age(self) -> int:
        return self.age + 1


@strawberry.experimental.pydantic.type(
    UserModel, all_fields=True, include_computed=True
)
class User:
    pass

Will allow nextAge to be requested from a user entity.

Here's the tweet text:

🆕 Release (next) is out! Thanks to Tyler Nisonoff for the PR 👏

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

@tylernisonoff
Copy link
Author

@patrick91 sorry for the ping, but would love to do whatever's needed to get this merged if this change would be accepted! Is there someone that can review this PR from the strawberry team?

@skilkis skilkis self-requested a review March 20, 2025 07:33
def get_model_fields(
self, model: type[BaseModel], include_computed: bool = False
) -> dict[str, CompatModelField]:
"""`include_computed` is a noop for PydanticV1Compat."""
new_fields = {}
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
new_fields = {}
del include_computed
new_fields = {}

Copy link
Contributor

Choose a reason for hiding this comment

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

Just to keep linters happy :)

@skilkis
Copy link
Contributor

skilkis commented Mar 20, 2025

@tylernisonoff looks good to me! Thanks for contributing this fix. Could you also please add a sentence to the pydantic docs. Something along the lines of:

If you want to include all of the fields from your Pydantic model, you can
instead pass `all_fields=True` to the decorator.

-> **Note** Care should be taken to avoid accidentally exposing fields that ->
weren't meant to be exposed on an API using this feature.

```python
import strawberry

from .models import User


@strawberry.experimental.pydantic.type(model=User, all_fields=True)
class UserType:
    pass

Addition below:

By default, computed fields are excluded. To also include all computed fields pass `include_computed=True` to the decorator.

```python
import strawberry

from .models import User


@strawberry.experimental.pydantic.type(model=User, all_fields=True, include_computed=True)
class UserType:
    pass

@skilkis skilkis closed this Mar 20, 2025
@skilkis
Copy link
Contributor

skilkis commented Mar 20, 2025

@tylernisonoff closing was a mistake 👀 I'll reopen if possible

@skilkis skilkis reopened this Mar 20, 2025
import strawberry


def test_computed_field():
Copy link
Contributor

Choose a reason for hiding this comment

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

Since we're using sets it won't be an issue, but it might be nice to add a test that has include_computed=True and also declares the computed field next_age with strawberry.auto to make sure we don't have duplication.

Copy link
Author

Choose a reason for hiding this comment

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

Pydantic doesn't seem to like me declaring both a computed field and another field, with or without the strawberry.auto type -- for example for

    class UserModel(pydantic.BaseModel):
        age: int
        next_age: strawberry.auto

        @computed_field
        @property
        def next_age(self) -> int:
            return self.age + 1

I get

ValueError: you can't override a field with a computed field

If you were thinking of something different though, let me know, happy to write another test!

Copy link
Contributor

@skilkis skilkis Mar 23, 2025

Choose a reason for hiding this comment

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

Ahh sorry for the confusion I meant something more along the lines of:

@strawberry.experimental.pydantic.type(
    UserModel, all_fields=True, include_computed=True
)
class User:
    next_age: strawberry.auto

This will touch the include_computed flow as well as the regular field specifier flow. Currently this test should pass fine as we are using a set to dedupe the fields, the purpose would be to check if ever this behavior changes in the future.

Not the most important test though for this PR as this can occur with or without computed fields, I'll already approve the PR, thanks again for contributing this! 🚀

Copy link
Contributor

@skilkis skilkis left a comment

Choose a reason for hiding this comment

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

Just waiting on that addition to the documentation, rest is great. If you have time an additional test-case for duplication with strawberry.auto would be nice. Thanks again for this fix, much appreciated 🚀

@tylernisonoff
Copy link
Author

@skilkis thanks a ton for reviewing! I added the documentation. Wasn't able to get the test you were suggesting working, but I may have misunderstood the approach. Happy to add more tests if you'd like.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Pydantic type all_fields does not include computed fields
3 participants