How do I correctly add type-hints to Mixin classes?

后端 未结 5 1814
梦谈多话
梦谈多话 2021-02-13 02:33

Consider the following example. The example is contrived but illustrates the point in a runnable example:

class MultiplicatorMixin:

    def multiply(self, m: in         


        
相关标签:
5条回答
  • 2021-02-13 02:48

    For reference, mypy recommends to implement mixins through a protocol (https://mypy.readthedocs.io/en/latest/more_types.html#advanced-uses-of-self-types).

    It works with mypy >= 750.

    from typing_extensions import Protocol
    
    
    class HasValueProtocol(Protocol):
        @property
        def value(self) -> int: ...
    
    
    class MultiplicationMixin:
    
        def multiply(self: HasValueProtocol, m: int) -> int:
            return self.value * m
    
    
    class AdditionMixin:
    
        def add(self: HasValueProtocol, b: int) -> int:
            return self.value + b
    
    
    class MyClass(MultiplicationMixin, AdditionMixin):
    
        def __init__(self, value: int) -> None:
            self.value = value
    
    0 讨论(0)
  • 2021-02-13 02:53

    One approach I saw in this question is type hinting the self attribute. Together with Union from the typing package, you are able to use the attributes from a class which is used together with your mixin, while still having correct type hinting for own attributes:

    from typing import Union
    
    class AdditionMixin:
    
        def add(self: Union[MyBaseClass, 'AdditionMixin'], b: int) -> int:
            return self.value + b
    
    
    class MyBaseClass:
    
        def __init__(self, value: int):
            self.value = value
    

    Downside is that you have to add the hint to every method, which is kind of cumbersome.

    0 讨论(0)
  • 2021-02-13 02:56

    I've tested it on my machine, hope it will also work for you:

    class MultiplicatorMixin:
        value = None # type: int
    
        def multiply(self, m: int) -> int:
            return self.value * m
    
    
    class AdditionMixin:
        value = None # type: int
    
        def add(self, b: int) -> int:
            return self.value + b
    
    
    class MyClass(MultiplicatorMixin, AdditionMixin):
    
        def __init__(self, value: int) -> None:
            self.value = value
    
    
    instance = MyClass(10)
    print(instance.add(2))
    print(instance.multiply(2))
    
    0 讨论(0)
  • 2021-02-13 02:59

    Try with:

    from typing import Type, TYPE_CHECKING, TypeVar
    
    T = TypeVar('T')
    
    
    def with_typehint(baseclass: Type[T]) -> Type[T]:
        """
        Useful function to make mixins with baseclass typehint
    
        ```
        class ReadonlyMixin(with_typehint(BaseAdmin))):
            ...
        ```
        """
        if TYPE_CHECKING:
            return baseclass
        return object
    
    

    Example tested in Pyright:

    class ReadOnlyInlineMixin(with_typehint(BaseModelAdmin)):
        def get_readonly_fields(self,
                                request: WSGIRequest,
                                obj: Optional[Model] = None) -> List[str]:
    
            if self.readonly_fields is None:
                readonly_fields = []
            else:
                readonly_fields = self.readonly_fields # self get is typed by baseclass
    
            return self._get_readonly_fields(request, obj) + list(readonly_fields)
    
        def has_change_permission(self,
                                  request: WSGIRequest,
                                  obj: Optional[Model] = None) -> bool:
            return (
                request.method in ['GET', 'HEAD']
                and super().has_change_permission(request, obj) # super is typed by baseclass
            )
    
    >>> ReadOnlyAdminMixin.__mro__
    (<class 'custom.django.admin.mixins.ReadOnlyAdminMixin'>, <class 'object'>)
    
    0 讨论(0)
  • 2021-02-13 03:06

    In addition to good answers mentioned above. My use case - mixins to be used in tests.

    As proposed by Guido van Rossum himself here:

    from typing import *
    T = TypeVar('T')
    
    class Base:
        fit: Callable
    
    class Foo(Base):
        def fit(self, arg1: int) -> Optional[str]:
            pass
    
    class Bar(Foo):
        def fit(self, arg1: float) -> str:
            pass    
    

    Thus, when it comes to a mixin, it could look as follows:

    
    class UsefulMixin:
    
        assertLess: Callable
        assertIn: Callable
        assertIsNotNone: Callable
    
        def something_useful(self, key, value):
            self.assertIsNotNone(key)
            self.assertLess(key, 10)
            self.assertIn(value, ['Alice', 'in', 'Wonderland']
    
    
    class AnotherUsefulMixin:
    
        assertTrue: Callable
        assertFalse: Callable
        assertIsNone: Callable
    
        def something_else_useful(self, val, foo, bar):
            self.assertTrue(val)
            self.assertFalse(foo)
            self.assertIsNone(bar)  
    

    And our final class would look as follows:

    class TestSomething(unittest.TestCase, UsefulMixin, AnotherUsefulMixin):
    
        def test_something(self):
            self.something_useful(10, 'Alice')
            self.something_else_useful(True, False, None)
    
    0 讨论(0)
提交回复
热议问题