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

Migrate graphql-transport-ws types to TypedDicts #3701

Conversation

DoctorJohn
Copy link
Member

@DoctorJohn DoctorJohn commented Nov 17, 2024

Description

I recently migrated the legacy ws protocols types to typed dicts (#3689), in this PR the modern ws protocols types are migrated to typed dicts. This, again, comes with some nice benefits such as:

  • precise modeling of null vs undefined values
  • no more need for custom .to_dict() methods on dataclasses to avoid performance penalties
  • no more need for UNSET conversions
  • more precise test assertions (that's were all the extra code comes from)
  • (the protocol implementation now also no longer uses Any)

Types of Changes

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

Summary by Sourcery

Enhancements:

  • Migrate graphql-transport-ws types from data classes to TypedDicts for better type precision and code simplification.

Copy link
Contributor

sourcery-ai bot commented Nov 17, 2024

Reviewer's Guide by Sourcery

This PR migrates the graphql-transport-ws protocol types from dataclasses to TypedDicts. The implementation focuses on using TypedDict's type system to more precisely model protocol message types, particularly distinguishing between null and undefined values. The changes eliminate the need for custom conversion methods and UNSET handling, while maintaining the same protocol functionality.

Class diagram for graphql-transport-ws TypedDicts

classDiagram
    class ConnectionInitMessage {
        type: "connection_init"
        payload: NotRequired<Union<Dict[str, object], None>>
    }

    class ConnectionAckMessage {
        type: "connection_ack"
        payload: NotRequired<Union<Dict[str, object], None>>
    }

    class PingMessage {
        type: "ping"
        payload: NotRequired<Union<Dict[str, object], None>>
    }

    class PongMessage {
        type: "pong"
        payload: NotRequired<Union<Dict[str, object], None>>
    }

    class SubscribeMessagePayload {
        query: str
        operationName: NotRequired<Union[str, None]>
        variables: NotRequired<Union<Dict[str, object], None>>
        extensions: NotRequired<Union<Dict[str, object], None>>
    }

    class SubscribeMessage {
        id: str
        type: "subscribe"
        payload: SubscribeMessagePayload
    }

    class NextMessagePayload {
        errors: NotRequired<List[GraphQLFormattedError]>
        data: NotRequired<Union<Dict[str, object], None>>
        extensions: NotRequired<Dict[str, object]>
    }

    class NextMessage {
        id: str
        type: "next"
        payload: NextMessagePayload
    }

    class ErrorMessage {
        id: str
        type: "error"
        payload: List[GraphQLFormattedError]
    }

    class CompleteMessage {
        id: str
        type: "complete"
    }

    class Message {
        <<Union>>
        ConnectionInitMessage
        ConnectionAckMessage
        PingMessage
        PongMessage
        SubscribeMessage
        NextMessage
        ErrorMessage
        CompleteMessage
    }
Loading

File-Level Changes

Change Details Files
Replace dataclass-based message types with TypedDict definitions
  • Convert all protocol message classes to TypedDict definitions
  • Add Literal types for message type fields to ensure type safety
  • Create a union type 'Message' to represent all possible message types
  • Remove custom as_dict() methods as they're no longer needed with TypedDict
strawberry/subscriptions/protocols/graphql_transport_ws/types.py
Update message handling logic to work with TypedDict messages
  • Modify message type checking to use dict access instead of class attributes
  • Update message construction to use dict literals instead of class instantiation
  • Improve type annotations for message handling functions
  • Remove UNSET value handling as it's no longer needed
strawberry/subscriptions/protocols/graphql_transport_ws/handlers.py
Update tests to use new TypedDict-based message format
  • Replace message class instantiations with direct dict construction
  • Add explicit type annotations for received messages in tests
  • Update message assertions to compare against dict literals
  • Improve test readability with better variable naming
tests/websockets/test_graphql_transport_ws.py
tests/channels/test_layers.py
tests/fastapi/test_context.py
tests/views/schema.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 @DoctorJohn - I've reviewed your changes and they look great!

Here's what I looked at during the review
  • 🟢 General issues: all looks good
  • 🟢 Security: all looks good
  • 🟢 Testing: all looks good
  • 🟡 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.

@botberry
Copy link
Member

botberry commented Nov 17, 2024

Thanks for adding the RELEASE.md file!

Here's a preview of the changelog:


In this release, we migrated the graphql-transport-ws types from data classes to typed dicts.
Using typed dicts enabled us to precisely model null versus undefined values, which are common in that protocol.
As a result, we could remove custom conversion methods handling these cases and simplify the codebase.

Here's the tweet text:

🆕 Release (next) is out! Thanks to @NucleonJohn for the PR 👏

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

Copy link

codecov bot commented Nov 17, 2024

Codecov Report

Attention: Patch coverage is 99.15612% with 4 lines in your changes missing coverage. Please review.

Project coverage is 96.70%. Comparing base (04936fd) to head (072a299).
Report is 10 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3701      +/-   ##
==========================================
- Coverage   97.01%   96.70%   -0.31%     
==========================================
  Files         502      502              
  Lines       33568    33608      +40     
  Branches     5634     5624      -10     
==========================================
- Hits        32565    32500      -65     
- Misses        796      885      +89     
- Partials      207      223      +16     
---- 🚨 Try these New Features:

@DoctorJohn DoctorJohn force-pushed the migrate-graphql-transport-ws-to-typeddicts branch from dea76f2 to 64ccfa9 Compare November 17, 2024 16:48
Copy link

codspeed-hq bot commented Nov 17, 2024

CodSpeed Performance Report

Merging #3701 will not alter performance

Comparing DoctorJohn:migrate-graphql-transport-ws-to-typeddicts (072a299) with main (4920f1e)

Summary

✅ 15 untouched benchmarks

@@ -198,11 +180,11 @@ async def handle_connection_init(self, message: ConnectionInitMessage) -> None:
return

self.connection_init_received = True
await self.send_message(ConnectionAckMessage())
await self.websocket.send_json(ConnectionAckMessage({"type": "connection_ack"}))
Copy link
Member

Choose a reason for hiding this comment

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

As much as I like the improvement stemming from the dicts above, I really dislike this redundancy of types. What about adding a new create classmethod on the TypedDict class that handles filling this value? Not very pythonic, but at least you don't need to pass the message types again

Copy link
Member Author

Choose a reason for hiding this comment

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

Similar story to the previous comment: At runtime TypedDicts are just regular dicts. When subclassing TypedDict we can't add any custom methods (or constants) to it.
While this new style looks more verbose, it's also more explicit since it's no longer hiding away any message fields. I honestly like it, especially since it makes pyright and mypy super happy.

Here's something we could do with the hightlighted code:
self.websocket.send_json takes any dict as argument, that's why we wrap our message dict in ConnectionAckMessage so that the type checker ensures it has a valid shape.
Instead we could add and call a (new) send_message(self, message: Message) method, which expects a valid Message as argument. In the example above, we would no longer need to wrap our typed dict in ConnectionAckMessage, because the type checker would ensure that the message's shape is valid based on it's type field.

So instead of:

await self.websocket.send_json(COnnectionAckMessage({"type": "connection_ack"})

we would write:

await self.send_message({"type": "connection_ack"})

and type checker would still provide autocompletion and enforce that messages have a valid shape.

Copy link
Member Author

Choose a reason for hiding this comment

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

(Just added two commits doing exactly that)

Copy link
Member

Choose a reason for hiding this comment

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

@DoctorJohn with your recent change, it looks a lot better. I like it!

Copy link
Member

Choose a reason for hiding this comment

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

My issue was mainly with the redundancy and verbosity of COnnectionAckMessage({"type": "connection_ack"}), but when reviewing I forgot to touch the possibility of just sending a normal dict :D Happy with this solution

@DoctorJohn DoctorJohn merged commit efc736f into strawberry-graphql:main Nov 18, 2024
107 of 109 checks passed
@DoctorJohn DoctorJohn deleted the migrate-graphql-transport-ws-to-typeddicts branch November 20, 2024 15:56
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.

4 participants