pythonpython-typinggoogle-api-python-client

Typehint googleapiclient.discovery.build returning value


I create the Google API Resource class for a specific type (in this case blogger).

from googleapiclient.discovery import Resource, build

def get_google_service(api_type) -> Resource:
    credentials = ...

    return build(api_type, 'v3', credentials=credentials)

def blog_service():
    return get_google_service('blogger')

def list_blogs():
    return blog_service().blogs()

The problem arises when using the list_blogs function. Since I am providing a specific service name, I know that the return value of blog_service has a blogs method, but my IDE doesn't recognize it. Is there a way to annotate the blog_service function (or any other part of the code) to help my IDE recognize the available methods like blogs?


Solution

  • The problem here is that the Resource class dynamically sets arbitrary attributes/methods based on what collections the underlying API provides.

    In your case apparently there is a blogs collection, which means the blogs method is dynamically constructed on that Resource object.

    Expecting static type annotations, when your types are created dynamically is a tall order. (I assume this is one of the reasons the maintainers of google-api-python-client do not even bother with type-hinting in that package.)

    But depending on your goals with those functions, you might improve your IDE experience by using protocols.

    If you know that your blog_service function returns an object that has a blogs method, you can define a corresponding protocol. The problem is of course kicked down the road because whatever that blogs method returns may also have arbitrary methods. But depending on if this is important to you, you can apply the same principle for that again.

    The upside is that Resource itself actually seems to have very few public methods, essentially just close and the context manager protocol. So you could emulate that in some sort of base protocol and use inheritance to construct different resource-protocols.

    Here is an example to illustrate:

    from typing import Any, Protocol, Self
    
    from googleapiclient.discovery import build  # type: ignore[import]
    
    
    class ResourceProtocol(Protocol):
        def close(self) -> None: ...
    
        def __enter__(self) -> Self: ...
    
        def __exit__(self, *args: Any) -> None: ...
    
    
    class BloggerProtocol(ResourceProtocol):
        def blogs(self) -> Any: ...
    
    
    def get_google_service(api_type: str) -> Any:
        credentials = ...
        return build(api_type, 'v3', credentials=credentials)
    
    
    def blog_service() -> BloggerProtocol:
        return get_google_service('blogger')  # type: ignore[no-any-return]
    
    
    def list_blogs() -> Any:
        return blog_service().blogs()
    

    (Note that those are all literal ellipses .... Also, if you are on Python <3.11, you should be able to import Self from typing_extensions instead.)

    Now your IDE should be able to detect that the object returned by blog_service has a blogs method and can be used as a context manager (using with).

    Notice that I annotated the return type of blogs as Any because I don't know what type it is supposed to return.