osmay88
osmay88

Reputation: 419

How to set a class method return typing dynamically from a class property

I'm working in a pet project of mine and found this little issue. I want to use typings withtin a base class, the problem is that the return type of the class methods are defined by a field set by the subclasses. Here is my base class

class BaseRepository(metaclass=RequiredAttributes('model')):

    model = None  # The subclasses have to define the this field with a class type.
                  # All the class methods will return objects of the same
                  # type as this field.

    def get(self, id) -> ??:
        # return an object of the type defined in mode
        ...


class UserRepo(BaseRepository): # subclass

    model = User # class type

    ...


I want to set the typing for the get function to be the same object type as defined in the model field.

Any suggestion on how can I acomplish something like that?

Upvotes: 1

Views: 1939

Answers (1)

Alex Waygood
Alex Waygood

Reputation: 7499

Without the metaclass, this passes MyPy, which is correctly able to infer that x is of type User. I wasn't able to test it with the metaclass, as I don't have your metaclass definition. The solution uses a parameterised Generic as a base class, which allows you to then assert to the type-checker that different methods across the class will have the same parameter and return argument types. A similar technique is used in Django's stub files.

N.B. You'll have to import Type from the typing module and use Optional[Type[M]] instead of Optional[type[M]] for your model annotation if you're using Python <= 3.8.

from typing import Optional, Generic, TypeVar


M = TypeVar('M')


class BaseRepository(Generic[M], metaclass=RequiredAttributes('model')):
    model: Optional[type[M]] = None     # The subclasses have to define the this field with a class type.
                                        # All the class methods will return objects of the same
                                        # type as this field.

    def get(self, id: int) -> M: ...
        # return an object of the type defined in mode
        

class User:
    pass


class UserRepo(BaseRepository[User]): # subclass
    model = User # class type
    

x = UserRepo().get(1)
reveal_type(x)  # Revealed type is "__main__.User*"

Upvotes: 6

Related Questions