Skip to content
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

Handling mixins that reference fields that they do not contain #5837

Closed
takeda opened this issue Oct 25, 2018 · 16 comments
Closed

Handling mixins that reference fields that they do not contain #5837

takeda opened this issue Oct 25, 2018 · 16 comments

Comments

@takeda
Copy link

takeda commented Oct 25, 2018

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.

@msullivan
Copy link
Collaborator

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.

@takeda
Copy link
Author

takeda commented Oct 25, 2018

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.

@JelleZijlstra
Copy link
Member

You can probably do that with something like:

if MYPY:
    _Base = SomeRealClass
else:
    _Base = object

class Mixin(_Base): ...

@takeda
Copy link
Author

takeda commented Oct 26, 2018

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?

@gvanrossum
Copy link
Member

You should use typing.TYPE_CHECKING. Using MYPY is only needed when you are stuck with a very old version of typing.py.

@takeda
Copy link
Author

takeda commented Oct 26, 2018

Awesome, thank you all for your help.

@takeda takeda closed this as completed Oct 26, 2018
@takeda takeda reopened this Oct 26, 2018
@takeda
Copy link
Author

takeda commented Oct 26, 2018

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"

@gvanrossum
Copy link
Member

gvanrossum commented Oct 26, 2018 via email

@takeda
Copy link
Author

takeda commented Oct 26, 2018

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()

@gvanrossum
Copy link
Member

gvanrossum commented Oct 26, 2018 via email

@takeda
Copy link
Author

takeda commented Oct 26, 2018

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.

@gvanrossum
Copy link
Member

I don't think we can help you with that...

kdrag0n added a commit to kdrag0n/pyrobud that referenced this issue Dec 15, 2019
This helps accomodate the growing Bot class, since it's easily divided
into several logical components anyway. Each component is an ABC
(Abstract Base Class) that functions as a mixin and thus can access all
the other attributes of the Bot class. Bot subclasses all the mixins to
unify them. Telethon uses the same approach to divide TelegramClient
into many mixins that each contain different categories of API methods.

An adaptive MixinBase reference that is defined as abc.ABC during
runtime and Bot during type checking is used as the subclass for each
mixin to work around python/mypy#5837.
Furthermore, the MixinBase reference itself has its type specified as
Any to work around python/mypy#2477.

The only change for outsiders should be the Bot class being moved to the
`core` module.

Signed-off-by: Danny Lin <danny@kdrag0n.dev>
@leonardoramirezr
Copy link

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'>)

@jaredlockhart
Copy link

@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:

#6372

which suggests that mypy does not accept dynamically generated base classes?

Also mypy complained that return object is an unreachable statement.

@jsatt
Copy link

jsatt commented Jul 28, 2020

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.

@BakinSergey
Copy link

choose 1. u can specify needed field with their typehints as classvariables of Mixin class. It's will be more pythonic(more explicit)
choose 2. u can inherit protocol (Typing.Protocol) of class which Mixin extended,
but this will not work if master class of mixin class inherit sqlalchemy Model :(

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants