Skip to content

Ensure field extensions are only applied once #3832

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

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

SupImDos
Copy link

@SupImDos SupImDos commented Apr 4, 2025

Description

Adds the apply_field_extensions utility function which uses @cache to ensure field extensions are only ever applied once, regardless of the number of schemas that use the field.

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

Prevent multiple applications of field extensions across different schema generations

New Features:

  • Introduced apply_field_extensions utility function with @cache decorator to ensure field extensions are applied only once

Bug Fixes:

  • Resolve an issue where field extensions were being applied multiple times when a field is used in multiple schemas, which could lead to unexpected behavior

Tests:

  • Added test cases to verify that field extensions are applied only once across multiple schema generations

Copy link
Contributor

sourcery-ai bot commented Apr 4, 2025

Reviewer's Guide by Sourcery

The pull request introduces a caching mechanism to ensure that field extensions are applied only once, even if a field is used in multiple schemas. This is achieved by adding a new utility function apply_field_extensions that uses the @cache decorator. The changes include modifications to the schema conversion process and the addition of new test cases to verify the correct application of field extensions.

Sequence diagram for applying field extensions with caching

sequenceDiagram
    participant SchemaConverter
    participant Field
    participant FieldExtension
    SchemaConverter->>Field: Get field extensions
    SchemaConverter->>apply_field_extensions: Call
    apply_field_extensions->>Field: Check cache
    alt Cache miss
        apply_field_extensions->>FieldExtension: Apply each extension
        FieldExtension-->>apply_field_extensions: Extension applied
        apply_field_extensions-->>Field: Cache result
    else Cache hit
        apply_field_extensions-->>Field: Return cached result
    end
Loading

Class diagram for the updated field extension application

classDiagram
    class StrawberryField {
        +List~FieldExtension~ extensions
    }
    class FieldExtension {
        +apply(field: StrawberryField)
    }
    class SchemaConverter {
        +wrap_field_extensions()
    }
    class Utility {
        +apply_field_extensions(field: StrawberryField)
    }
    StrawberryField --> FieldExtension : uses
    SchemaConverter --> Utility : calls
    Utility --> FieldExtension : applies
    note for Utility "apply_field_extensions is cached to ensure single application"
Loading

File-Level Changes

Change Details Files
Introduce caching mechanism for field extensions to ensure they are applied only once.
  • Added apply_field_extensions function with @cache decorator to apply field extensions only once.
  • Modified schema conversion logic to use apply_field_extensions instead of applying extensions directly.
strawberry/extensions/field_extension.py
strawberry/schema/schema_converter.py
Add test cases to verify that field extensions are applied only once.
  • Added a test to ensure that field extensions are not applied multiple times across different schemas.
  • Created a custom extension to track the number of times it is applied and verified it is applied only once.
tests/relay/test_schema.py
tests/schema/extensions/test_field_extensions.py
Document the release changes related to the caching mechanism for field extensions.
  • Added a release note describing the fix for field extensions being applied multiple times.
RELEASE.md

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
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.

Hey! Thanks for the PR!

I wonder if we can maybe scope the cache to the schema? Or maybe when applying the extensions we make a copy of the field?

Just think about a future in which we might have default field extensions (based on the schema) and fields on different schemas will have different fields, what do you think?

Copy link

codecov bot commented Apr 4, 2025

Codecov Report

Attention: Patch coverage is 91.42857% with 3 lines in your changes missing coverage. Please review.

Project coverage is 95.13%. Comparing base (c7910e0) to head (3637200).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3832      +/-   ##
==========================================
- Coverage   95.14%   95.13%   -0.02%     
==========================================
  Files         500      500              
  Lines       32463    32494      +31     
  Branches     1684     1685       +1     
==========================================
+ Hits        30886    30912      +26     
- Misses       1311     1315       +4     
- Partials      266      267       +1     
🚀 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.

Copy link

codspeed-hq bot commented Apr 4, 2025

CodSpeed Performance Report

Merging #3832 will not alter performance

Comparing BrightpathProgress:bugfix/apply-field-extensions-once (3637200) with main (c7910e0)

Summary

✅ 21 untouched benchmarks

@bellini666
Copy link
Member

I wonder if we can maybe scope the cache to the schema? Or maybe when applying the extensions we make a copy of the field?

Was thinking that exactly! Scope the cache to the schema somehow (either by having it as a dict[StrawberryField, list[Extensions]] in the schema or dict[Schema, list[Extension]] in the field itself)

Copying the field is an option, but might generate other issues?

@SupImDos
Copy link
Author

SupImDos commented Apr 7, 2025

I wonder if we can maybe scope the cache to the schema? Or maybe when applying the extensions we make a copy of the field?

Just think about a future in which we might have default field extensions (based on the schema) and fields on different schemas will have different fields, what do you think?

Was thinking that exactly! Scope the cache to the schema somehow (either by having it as a dict[StrawberryField, list[Extensions]] in the schema or dict[Schema, list[Extension]] in the field itself)

Copying the field is an option, but might generate other issues?

Hi @patrick91 & @bellini666 , thanks so much for your replies!

I actually considered both of these for this PR, and decided against it for my initial implementation - hopefully I can explain why below.


1. Scoping the cache to the schema

Maybe I'm missing something, but I don't think this will actually work in isolation.

The issue we're trying to solve here isn't that a single schema will apply the extensions multiple times - its that currently the same field instances are shared across multiple schemas, so each schema will apply the field extensions once. So, if we cache the extensions per-schema, we will still end up applying field extensions to the same field multiple times (once per-schema).

However, I do agree with the reasoning here - if in the future we have default field extensions that are schema-specific, we may need to cache them per-schema.

2. Copying the field before applying the extensions

This does seem to be a valid approach, and I think it would work (and would even work in conjunction with a schema-scoped cache).

However, I was worried about whether this might be a bit of a hack. Do we know if there are any flow-on effects of doing this? For example, is there code relying on there being only one instance of a field per schema, or code that relies on the field being the same instance after schema generation? Maybe there is even downstream code relying on this?

If we aren't concerned about that - and copying fields before applying extensions is valid - then maybe this is the way to go?


Does my reasoning above make sense and/or give you any ideas? Or am I missing something in my understanding above? 😅

@SupImDos
Copy link
Author

SupImDos commented Apr 8, 2025

@patrick91 & @bellini666

Sorry for the immediate follow-up, but I've got some extra context that might influence our decision / implementation.

I looked into copying fields in my project, and I've realised that I have custom StrawberryField subclasses that don't support copying properly! As a contrived example:

class MyCustomStrawberryField(StrawberryField):
    def __init__(self, *args, my_custom_arg, **kwargs):
        super().__init__(*args, **kwargs)
        self.something = self.do_some_processing_with(my_custom_arg)

This example subclass:

  1. Has a required my_custom_arg which is incompatible with the base StrawberryField.__copy__ method
  2. Processes my_custom_arg which is incompatible with the base StrawberryField.__copy__ method
  3. Doesn't implement its own __copy__ method to copy its own custom attributes

I have also found that the base StrawberryField.__copy__ method implementation doesn't provide a way to contribute arguments when instantiating the copy, which makes fixing this a bit tricky.

Regardless, this is probably a design issue with my subclass - but its made me wonder whether similar downstream issues might be encountered if we choose to copy the fields here?

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