This is more of a question or a feature request if there's not a good way to do it.
I'm currently adding types to existing python 2.7 code base hoping that it will help me with migrating the code to Python 3.
I run into code that looks like this:
#! /usr/bin/env python
class A(object):
def __init__(self, var1, var2):
# type: (str, str) -> None
self.var1 = var1
self.var2 = var2
class Mixin(object):
def funcA(self):
# type: () -> str
return self.var1
class B(A, Mixin):
pass
def main():
# type: () -> None
a = B('value1', 'value2')
print(a.funcA())
if __name__ == '__main__':
main()
This is valid Python code, i.e.:
$ ./test.py
value1
But mypy is complaining:
$ ../py37/bin/mypy --python-executable ../py27/bin/python test.py
test.py:12: error: "Mixin" has no attribute "var1"
I understand the issue, there's nothing in Mixin class that states that var1 will be available.
My question is is there a way to tell mypy somehow that this attribute will be available?
I did not have much luck with using abstract classes and Protocol.
I also had a suggestion to use stub file, but that doesn't look like be perfect solution either, since I might need to define types outside of the module and keep them in sync. And this pattern is used in multiple places not just one.
Is there a way how this could be expressed in mypy? If the code needs to be slightly refactored without changing how it works I suppose that might also be acceptable.
How about (for python 2.7; in python 3.6 or greater you'd use a variable type annotation):
MYPY = False
class Mixin(object):
if MYPY:
var1 = None # type: str
def funcA(self):
# type: () -> str
return self.var1
(The if MYPY stuff isn't strictly necessary, but it keeps the runtime behavior from changing.)
I had hoped it would work to declare var1 as an abstractproperty of Mixin, but mypy doesn't seem able to figure out they are compatible.
That actually will work, and probably will do it this way, but if there was a way to define some parent class that only mypy would see would be perfect. So I would need to define these fields only once.
You can probably do that with something like:
if MYPY:
_Base = SomeRealClass
else:
_Base = object
class Mixin(_Base): ...
That actually seems to work rather well, at least for the code so far. I used TYPE_CHECKING from typing instead of MYPY, is there any disadvantage to use that instead?
You should use typing.TYPE_CHECKING. Using MYPY is only needed when you are stuck with a very old version of typing.py.
Awesome, thank you all for your help.
Actually looks like this still has an issue. When I'm now checking for classes that includes mixins I'm getting error similar to this: test.py:21: error: Cannot determine consistent method resolution order (MRO) for "B"
--Guido (mobile)
This is how the code looks now after the change:
#! /usr/bin/env python
from typing import TYPE_CHECKING
class A(object):
def __init__(self, var1, var2):
# type: (str, str) -> None
self.var1 = var1
self.var2 = var2
if TYPE_CHECKING:
_Base = A
else:
_Base = object
class Mixin(_Base):
def funcA(self):
# type: () -> str
return self.var1
class B(A, Mixin):
pass
def main():
# type: () -> None
a = B('value1', 'value2')
print(a.funcA())
if __name__ == '__main__':
main()
Yeah, if I place mixin first then things work and I no longer even need to make type checking a special case, just have the base class a parent of mixin by default. Seems like that is the correct way of using mixins. My only concern is that this is an existing code base and I'm wondering if this could break something this way.
I don't think we can help you with that...
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:
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'>)
@leonardon473 trying your proposal results in:
Unsupported dynamic base class "with_typehint" mypy(error)
using:
Python 3.8.2
mypy==0.770
The closest reference I find to that error is here:
https://github.com/python/mypy/issues/6372
which suggests that mypy does not accept dynamically generated base classes?
Also mypy complained that return object is an unreachable statement.
An alternate solution for future folks which this doesn't work:
I was having a similar problem making a QuerySet Mixin for Django.
class Mixin:
def do_stuff(self, value: str) -> QuerySet:
return self.filter(abc=value)
Resulted in "Mixin" has no attribute "filter"
So I tried the solution above:
if TYPE_CHECKING:
_Base = QuerySet
else:
_Base = object
class Mixin(_Base):
def do_stuff(self, value: str) -> QuerySet:
return self.filter(abc=value)
And got Invalid base class "_Base"
Not sure why it's failing with QuerySet, but also with Model and Field based classes. Most other classes seem to work fine, just not those (maybe something in the metaclasses Django uses?).
I finally got it to work by type hinting self,
class Mixin:
def do_stuff(self: QuerySet, value: str) -> QuerySet:
return self.filter(abc=value)
If anyone has feedback as to why the other ways aren't working I'd be happy to hear about it (CPYthon 3.8.0, Mypy: 0.780), but this solution worked and is also more succinct that the others.
Most helpful comment
An alternate solution for future folks which this doesn't work:
I was having a similar problem making a QuerySet Mixin for Django.
Resulted in
"Mixin" has no attribute "filter"So I tried the solution above:
And got
Invalid base class "_Base"Not sure why it's failing with QuerySet, but also with Model and Field based classes. Most other classes seem to work fine, just not those (maybe something in the metaclasses Django uses?).
I finally got it to work by type hinting
self,If anyone has feedback as to why the other ways aren't working I'd be happy to hear about it (CPYthon 3.8.0, Mypy: 0.780), but this solution worked and is also more succinct that the others.