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

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

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

@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


if message_type == ConnectionInitMessage.type:
await self.handle_connection_init(ConnectionInitMessage(**message))
elif message["type"] == "ping":
Copy link
Member

Choose a reason for hiding this comment

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

Why are we switching from constants to strings here? I get that these strings are obvious, but my intuition tells me a constant suits this better

Copy link
Member Author

@DoctorJohn DoctorJohn Nov 17, 2024

Choose a reason for hiding this comment

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

I want the new TypedDicts to be the single source of truth for the shape of graphql-transport-ws messages. However, unlike with dataclassses, it's not possible to store a constant value in a TypedDict. The only option is to use typing.Literal and let the type checker enforce that message["type"] must always match one of the literal message types. (which is what we do here).

Just to clarify: the right side of the == has autocompletion and type checkers riot if a value is put there which does not match any of the message types.

Copy link
Member

Choose a reason for hiding this comment

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

I think with types, plain strings are better 😊

@@ -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)

strawberry/channels/testing.py Show resolved Hide resolved

if message_type == ConnectionInitMessage.type:
await self.handle_connection_init(ConnectionInitMessage(**message))
elif message["type"] == "ping":
Copy link
Member

Choose a reason for hiding this comment

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

I think with types, plain strings are better 😊

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