Skip to content

_TableT is not compatible with TypedDict #596

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

Open
2 tasks done
Glinte opened this issue May 16, 2025 · 2 comments
Open
2 tasks done

_TableT is not compatible with TypedDict #596

Glinte opened this issue May 16, 2025 · 2 comments
Labels
bug Something isn't working

Comments

@Glinte
Copy link

Glinte commented May 16, 2025

Bug report

  • I confirm this is a bug with Supabase, not with my own application.
  • I confirm I have searched the Docs, GitHub Discussions, and Discord.

Describe the bug

from ..version import __version__
from .request_builder import AsyncRequestBuilder, AsyncRPCFilterRequestBuilder
_TableT = Dict[str, Any]
class AsyncPostgrestClient(BasePostgrestClient):

_TableT should be MutableMapping[str, Any] (Edit: Mapping[str, Any]) instead of dict[str, Any] to allow for users to specify a TypedDict as the output type. This seems to be a new-ish regression because my type checker wasn't complaining 4 months ago (edit: this is because there was no _TableT in postgrest 0.19.3 )

Issues from the two main type checkers explaining why dict is not compatible with TypedDict by design
mypy: python/mypy#4976
pyright: microsoft/pyright#6658

To Reproduce

Image

async def f():
    db = AsyncClient(supabase_url, supabase_key, options)
    response: APIResponse[MessageRecord] = await db.table("messages").select("*").execute()

class MessageRecord(TypedDict):
    content: str | None

Expected behavior

Using a TypedDict should be accepted

System information

  • OS: Windows
  • postgrest-py: 1.0.1
@Glinte Glinte added the bug Something isn't working label May 16, 2025
@Glinte Glinte changed the title _TableT is not compatible with TypedDict due to using dict instead of MutableMapping _TableT is not compatible with TypedDict May 16, 2025
@silentworks
Copy link
Contributor

I've had a look at this and even with the change your propose it's still not accepting the new type defined on the returned response. What I've found to work is to not worry about the type at the .execute() level but rather do the type conversion with a Pydantic model by dumping the response to a model.

import os
import asyncio
from typing import Annotated

from dotenv import load_dotenv
from postgrest.exceptions import APIError
from postgrest._async.client import AsyncPostgrestClient
from pydantic import BaseModel, Field

load_dotenv()

url = os.environ.get("SUPABASE_URL", "")
key = os.environ.get("SUPABASE_KEY", "")

class Country(BaseModel):
    id: Annotated[int, Field(alias="id")]
    name: Annotated[str, Field(alias="name")]

def Countries(data: list):
    return [Country(**x) for x in data]

class CountryResponse(BaseModel):
    count: Annotated[int | None, Field(alias="count")]
    data: Annotated[list[Country], Field(alias="data")]

async def main():
    headers = {"Authorization": f"Bearer {key}"}
    async with AsyncPostgrestClient(base_url=f"{url}/rest/v1", headers=headers) as client:
        try:
            resp = (await client.table("countries").select("*").execute()).model_dump()
            response = CountryResponse(**resp)

            for row in response.data:
                print(f"RESPONSE: {row.id} {row.name}")
        except APIError as e:
            print(f"ERROR: {e}")

asyncio.run(main())

This might not be ideal for you but this is the current working solution I could come up with. I will investigate more to see how we can coerce the type a bit more to follow your original setup.

@Glinte
Copy link
Author

Glinte commented May 19, 2025

Seems to be impossible to do inline type hint unless PEP 718 Subscriptable functions is accepted. I checked by reverted to the old version of postgrest that I was using, the reason why it worked before is because there was no _TableT so pyright just assumed Unknown i.e. Any in another name, and allows me to subscript with whatever type I want.

Reason why I believe coercing the type to my original setup is impossible is because

_TableT = Dict[str, Any]
AsyncPostgrestClient.from_(self, table: str) -> AsyncRequestBuilder[_TableT]: ...

has defined already defined the output type through the chain of classes, which passes _TableT into APIResponse[_TableT]

class APIResponse(BaseModel, Generic[_ReturnT]):
    data: List[_ReturnT]
    """The data returned by the query."""

and _ReturnT is invariant because data is list[_ReturnT] and the type parameter inside lists is invariant. What is being done right now means APIReponse.data must always be assumed to be Dict[str, Any]. Nothing that is done afterwards short of a cast can matter

Here are 3 ways that I came up that was close to what

  1. Split into two lines. This works out of the box.
table: AsyncRequestBuilder[MessageRecord] = db.table("messages")
response = await table.select("*").execute()
  1. Wrapping over AsyncClient/Client to add custom overloads to tables for each known table_name. This still requires _TableT = Mapping[str, Any]
class DatabaseManager(AsyncClient):
    """Wrapper class for the supabase client."""
    @overload
    def table(self, table_name: Literal["messages"]) -> AsyncRequestBuilder[MessageRecord]: ...

    @overload
    def table(self, table_name: str) -> AsyncRequestBuilder[Mapping[str, Any]]: ...

    @override
    def table(self, table_name: str) -> AsyncRequestBuilder[Mapping[str, Any]]:
        """Override the table method to add the correct type hints via overloads."""
        return self.postgrest.from_(table_name)
  1. Hope PEP 718 Subscriptable functions comes out and be able to do, changing _TableT to a TypeVar with default
response = await db.table[MessageRecord]("messages").select("*").execute()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants