feat(functions): refactor sync and async methods #1338
Draft
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
What kind of change does this PR introduce?
Preview for the sync vs async rewrite proposal, using
functionsas the first example. The idea for this rewrite is that we can refactor the methods in such a way to only write the core logic once, and derive both sync and async implementations out of the same client.To understand the rewrite, we should study the following hypothetical method, and its async counterpart:
We see that the only differences between the sync and async implementation is the
asyncandawaitkeywords. In this PR, I propose to rewrite these two into a single implementation, by making removing the IO out of the methods:With this, we can write the
list_somethingmethod only once, as a description for how to send a request and how to parse a response, and execute it both ways, either sync or async.However, in order to execute it in this way, we'd need to externally call the client in the executor:
This would work, but would be cumbersome and tiring to use. Instead, we can a store an executor inside the client, and use a decorator to blindly call
communicateon the endpoint. Regardless of which kind of IO, this will return the correct object, and will work correctly.In order to derive the sync and async clients, all we'd need to do is pass the executor we want:
However, how do we convince mypy that this is the case?
Typing
In order to show mypy that the type of
list_something()depends on the executor kind, we need to make add the executor as a generic type parameter ofSupabaseClient, and use@overloadson thehttp_endpointto decide whether it should be sync or async:Even though this seems complex, its not. Because the typing is not easy to write, I rewrote the
http_endpointdecorator to be a dataclass class, so that I could annotate it more easily. Thus, you can interpret it in the following way:Success | Awaitable[Sucess]SyncExecutor, the return is the sync version,SuccessAsyncExecutor, the return is the sync version,Awaitable[Sucess]The
ParamSpecpart is used to be generic over all the possible args and kwargs of any method passed in. Similarly, theHasExecutorprotocol is used to be generic over all the possible clients that can be passed in, so that we can share this decorator over multiple clients -- eg. SupabaseVectorClient and GoTrueClient.With this, mypy is satisfied, and
http_endpointtype checks for both the sync and async clients. Using the functions example in this PR:[In fact, this is a form of Higher Kinded Types in Python!
HasExecutoris a family of functor types. If you're interested, you can read more here: https://sobolevn.me/2020/10/higher-kinded-types-in-python].