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

feat(schema): semantic nullability draft #3722

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

Conversation

erikwrede
Copy link
Member

@erikwrede erikwrede commented Dec 11, 2024

This PR brings experimental support for semantic nullability to strawberry, as discussed with @captbaritone, @patrick91.

Open tasks

  • Field Extension rework: Make field extensions modifiable after a resolver is generated. Currently, we need to infer the type of the field to generate the resolver, including all field extensions & middleware, then read the type of the field again in case an extension changed it. Only then we can make final conclusions about the nullability of a field. Now we need to add another extension for the nullability assertion (will track in separate PR)
  • Granular level control - currently we only support the default option, marking a list field as [Int]. With levels support, we can also support [Int!] or [Int]!

For further info on semantic nullability, please check out this excellent talk by Jordan: https://www.youtube.com/watch?v=kVYlplb1gKk

Copy link
Contributor

sourcery-ai bot commented Dec 11, 2024

Reviewer's Guide by Sourcery

This PR introduces semantic nullability support in Strawberry by adding a new Required type and implementing the necessary schema conversion logic. The implementation includes a beta feature flag and a semantic non-null directive to handle required fields differently from regular non-null fields.

Class diagram for new and modified classes

classDiagram
    class StrawberryRequired {
        <<StrawberryContainer>>
    }
    class SemanticNonNull {
        +list<int> levels
    }
    class StrawberryAnnotation {
        +create_required(evaled_type: Any) StrawberryRequired
        +_is_required(annotation: Any) bool
    }
    class SchemaConverter {
        +from_maybe_optional_or_required(type_: Union<StrawberryType, type>) Union<GraphQLNullableType, GraphQLNonNull>
        +create_semantic_non_null_directive() SemanticNonNull
    }
    class StrawberryConfig {
        +bool semantic_nullability_beta
    }
    StrawberryAnnotation --> StrawberryRequired
    SchemaConverter --> SemanticNonNull
    SemanticNonNull --> StrawberryConfig
    note for StrawberryRequired "New class for handling required types"
    note for SemanticNonNull "New class for semantic non-null directive"
    note for SchemaConverter "Modified to handle semantic nullability"
Loading

File-Level Changes

Change Details Files
Added support for semantic nullability with a new Required type
  • Introduced StrawberryRequired container class
  • Added _is_required method to check for Required type annotations
  • Implemented create_required method to handle Required type creation
strawberry/types/base.py
strawberry/annotation.py
Modified schema conversion to handle semantic non-null fields
  • Renamed from_maybe_optional to from_maybe_optional_or_required
  • Added support for StrawberryRequired type conversion
  • Added semantic non-null flag to GraphQLNonNull types
strawberry/schema/schema_converter.py
Added semantic nullability configuration and directive
  • Added semantic_nullability_beta flag to StrawberryConfig
  • Created SemanticNonNull directive class
  • Added logic to append semantic non-null directive to fields when feature is enabled
strawberry/schema/config.py
strawberry/types/semantic_non_null.py
Added tests for semantic nullability feature
  • Created test case for semantic nullability with Product type
  • Added validation for SDL output
  • Included _Entity type query test
tests/schema/test_semantic_nullability.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.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a 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. You can also use
    this command to specify where the summary should be inserted.

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

Overall Comments:

  • Consider adding more test cases to cover edge cases and different combinations of Required/Optional types, particularly around nested types and lists.
Here's what I looked at during the review
  • 🟡 General issues: 1 issue found
  • 🟢 Security: all looks good
  • 🟡 Testing: 2 issues found
  • 🟢 Complexity: all looks good
  • 🟢 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.

NoneType = type(None)
if type_ is None or type_ is NoneType:
return self.from_type(type_)
elif isinstance(type_, StrawberryOptional):
return self.from_type(type_.of_type)
elif isinstance(type_, StrawberryRequired):
Copy link
Contributor

Choose a reason for hiding this comment

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

issue (bug_risk): Missing return statement in StrawberryRequired branch

The code sets strawberry_semantic_required_non_null but doesn't return graphql_core_type, causing it to fall through to the else clause. Add 'return graphql_core_type' after setting the attribute.

from strawberry.schema.config import StrawberryConfig


def test_entities_type_when_no_type_has_keys():
Copy link
Contributor

Choose a reason for hiding this comment

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

issue (testing): Test name doesn't match the actual test content

The test name suggests it's testing entity types without keys, but it appears to be testing semantic nullability behavior. Consider renaming the test to something like test_semantic_nullability_schema_generation or test_semantic_nullability_with_required_and_optional_fields to better reflect its purpose.

Comment on lines 13 to 14
price: Required[int]
weight: Optional[int]
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): Missing test cases for semantic nullability behavior

The test only verifies SDL generation but doesn't test the runtime behavior of Required and Optional fields. Consider adding test cases that verify: 1) Required fields actually enforce non-null values at runtime, 2) Optional fields accept null values, 3) Error cases when null is provided for Required fields.

Suggested implementation:

def test_entities_type_when_no_type_has_keys():
    """Test SDL generation for types with Required and Optional fields"""
=======

def test_required_field_rejects_null():
    """Test that Required fields raise an error when null is provided"""
    @strawberry.type
    class Product:
        upc: str
        name: str
        price: Required[int]
        weight: Optional[int]

    schema = strawberry.Schema(
        query=Product,
        config=StrawberryConfig(semantic_nullability_beta=True)
    )

    with pytest.raises(ValueError) as exc:
        Product(upc="123", name="test", price=None, weight=10)
    assert "price cannot be null" in str(exc.value)

def test_optional_field_accepts_null():
    """Test that Optional fields accept null values"""
    @strawberry.type
    class Product:
        upc: str
        name: str
        price: Required[int]
        weight: Optional[int]

    # Should not raise any errors
    product = Product(upc="123", name="test", price=100, weight=None)
    assert product.weight is None

def test_required_field_accepts_value():
    """Test that Required fields accept non-null values"""
    @strawberry.type
    class Product:
        upc: str
        name: str
        price: Required[int]
        weight: Optional[int]

    # Should not raise any errors
    product = Product(upc="123", name="test", price=100, weight=10)
    assert product.price == 100

You'll need to:

  1. Add import pytest at the top of the file if not already present
  2. Make sure the Required and Optional types are properly imported from wherever they are defined in your codebase
  3. Consider adding more edge cases based on your specific requirements (e.g., testing with different types, testing inheritance behavior, etc.)

@botberry
Copy link
Member

botberry commented Dec 11, 2024

Hi, thanks for contributing to Strawberry 🍓!

We noticed that this PR is missing a RELEASE.md file. We use that to automatically do releases here on GitHub and, most importantly, to PyPI!

So as soon as this PR is merged, a release will be made 🚀.

Here's an example of RELEASE.md:

Release type: patch

Description of the changes, ideally with some examples, if adding a new feature.

Release type can be one of patch, minor or major. We use semver, so make sure to pick the appropriate type. If in doubt feel free to ask :)

Here's the tweet text:

🆕 Release (next) is out! Thanks to Erik Wrede for the PR 👏

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

Copy link

codecov bot commented Dec 11, 2024

Codecov Report

Attention: Patch coverage is 53.84615% with 24 lines in your changes missing coverage. Please review.

Project coverage is 94.98%. Comparing base (275ebc1) to head (861d125).
Report is 16 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3722      +/-   ##
==========================================
- Coverage   97.01%   94.98%   -2.04%     
==========================================
  Files         500      499       -1     
  Lines       33399    32381    -1018     
  Branches     5601     1664    -3937     
==========================================
- Hits        32402    30756    -1646     
- Misses        793     1357     +564     
- Partials      204      268      +64     

Copy link

codspeed-hq bot commented Dec 11, 2024

CodSpeed Performance Report

Merging #3722 will not alter performance

Comparing erikwrede:feat/semantic_nullability (861d125) with main (6553c9e)

Summary

✅ 15 untouched benchmarks

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.

2 participants