Skip to content

Specify type when type variables are remained in inherited classmethod. #5420

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

Closed
wants to merge 1 commit into from

Conversation

kitsuyui
Copy link

@kitsuyui kitsuyui commented Aug 5, 2018

Fix: #3645

- Fix: 1337, 3645

@ilevkivskyi ilevkivskyi self-requested a review August 5, 2018 17:42
@ilevkivskyi ilevkivskyi self-assigned this Aug 5, 2018
Copy link
Member

@ilevkivskyi ilevkivskyi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR!

This may be not the best first issue to fix, but if you are ambitious, then here are few more comments about what to do:

  1. Add tests for all patterns mentioned in the issues you want to close
  2. Add more sophisticated tests that test your solution in edge cases (e.g. multiple inheritance bases, multiple inheritance levels, overriding, binding one type variable to another, etc.)
  3. In @classmethod of Generic class #1337 there is also a question what to do with class attributes (in addition to class methods), you should address this. Namely,
class C(Generic[T]):
    x: T
class D(C[int]): ...
D.x  # should not error and result in 'builtins.int'
  1. I think a "proper" fix for these issues should also add some more changes. For example, this currently correctly generates an error about ambiguity:
class C(Generic[T]):
    x: T
reveal_type(C.x)

but the revealed type should be Any, type variables should not "leak out" even in case of errors to avoid more spurious errors.

@kitsuyui
Copy link
Author

kitsuyui commented Aug 7, 2018

@ilevkivskyi
Thank you for reviewing my codes.
I will try that you pointed!

@kitsuyui kitsuyui force-pushed the generic-classmethod-type branch 4 times, most recently from ce6fea2 to e59c7ae Compare August 11, 2018 04:43
@kitsuyui
Copy link
Author

@ilevkivskyi

In #1337 there is also a question what to do with class attributes (in addition to class methods), you should address this. Namely,

class C(Generic[T]):
    x: T
class D(C[int]): ...
D.x  # should not error and result in 'builtins.int'

I think this sample should be like this

class C(Generic[T]):
    x: List[T] = []
class D(C[int]): pass
D.x # should not error and result in 'builtins.int' 

Because x: T become type hint for instance attribute.
So I will test x: List[T] = [] as class attribute.

@kitsuyui
Copy link
Author

Sorry, I've tried and I understood it will not be solved for class attributes.

Class attributes are shared with super-class. So subclasses cannot be restricted to narrow type.

from typing import Generic, TypeVar, List

T = TypeVar('T')
class A(Generic[T]):
    x: List[T] = []
class B(A[int]): pass
class C(A[str]): pass
B.x.append(1)
C.x.append('1')
print(B.x)  # => [1, '1']
print(C.x)  # => [1, '1']

@ilevkivskyi
Copy link
Member

Sorry for a delay. I will try to review this PR next week. In the meantime could you please fix the merge conflict?

@kitsuyui kitsuyui force-pushed the generic-classmethod-type branch from 633cd53 to a9de04c Compare August 19, 2018 07:44
@kitsuyui
Copy link
Author

@ilevkivskyi I've updated!

@ilevkivskyi
Copy link
Member

Hi @kitsuyui! Sorry for a delay with review, are you still interested in working on this?

@kitsuyui
Copy link
Author

kitsuyui commented Oct 2, 2018

@ilevkivskyi
Yeah, I'd love to.
I will challenge updating that it works even in case of #5664 if needed.

Copy link
Member

@ilevkivskyi ilevkivskyi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, let us give this one more chance. The general idea is good, but the implementation requires polishing. I left some comments below.

if base_instance.type.bases:
typ = expand_type_by_instance_bases(typ, base_instance)
typ = expand_type_by_instance(typ, base_instance)
return typ
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This algorithm currently looks wrong, and this is why #5664 and similar more complex type variable bindings don't work. You need to map type variables correctly, look for example in maptype.py

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The algorithm is still wrong. For example it will not work for something like this:

class B(Generic[T]):
    @classmethod
    def f(cls) -> T: ...
class C(B[Tuple[T, T]]):
    ...
class D(C[str]):
    ...
reveal_type(D.f())  # Revealed type is 'str', but should be 'Tuple[str, str]'

This is not just a small bug there, it is "conceptually" wrong. As I already said twice, you should use something similar to map_instance_to_supertype from maptype.py. If this is too complex, then I can overtake this PR, it already takes too long.

@oraluben
Copy link

oraluben commented Oct 7, 2018

I've tried the latest code and found something that may be added to the test cases, here's the code.

from typing import Generic, Callable, TypeVar, Iterator, Type

T = TypeVar('T')
BoxT = TypeVar('BoxT', bound='Box', covariant=True)


class Box(Generic[T]):
    @classmethod
    def wrap(cls, generator: Callable[[], T]) -> 'Box[T]':
        pass


class IteratorBox(Box[Iterator[T]],
                  Generic[T]):
    @classmethod
    def wrap_iter(cls: Type[BoxT],
                  typ: Type[Box[T]]) -> Callable[[Callable[[], Iterator[T]]], BoxT]:
        def inner(generator: Callable[[], Iterator[T]]) -> BoxT:
            pass

        return inner


class Str(Box[str]):
    pass


class StrI(IteratorBox[str]):
    pass


@StrI.wrap_iter(Str)
def g() -> Iterator[str]:
    pass

I believe there's no type error in the signature, but the output was

test.py:32: error: Argument 1 has incompatible type "Callable[[], Iterator[str]]"; expected "Callable[[], Iterator[Iterator[str]]]"

test.py:32: error: Argument 1 to "wrap_iter" of "IteratorBox" has incompatible type "Type[Str]"; expected "Type[Box[Iterator[str]]]

Hope this will help!

@kitsuyui
Copy link
Author

kitsuyui commented Oct 7, 2018

@oraluben

Your code works on my local mypy! But I still have not pushed commits yet.
The biggest uncompleting task is about handling error and replacing unbound variable to Any with consistency.

@kitsuyui kitsuyui force-pushed the generic-classmethod-type branch from a9de04c to 4540a12 Compare October 8, 2018 10:20
@kitsuyui
Copy link
Author

kitsuyui commented Oct 8, 2018

I have pushed commits that rebased from latest master.

But this is still insufficient yet.

I have been still in the way of this part: #5420 (comment)
So #5664 is not solved too.

It might be little more time to solve.

@ilevkivskyi
Copy link
Member

@kitsuyui what is exactly the problem you have? I might be able to give some hints.

@kitsuyui
Copy link
Author

kitsuyui commented Oct 13, 2018

@ilevkivskyi

I don't know what is the true behavior on following case;

from typing import Generic, TypeVar
T = TypeVar('T')
class A(Generic[T]):
    @classmethod
    def f(cls, value: T) -> T: pass
reveal_type(A.f(1))  # : Revealed type is 'builtins.int*'

T is solved by arguments. But T is not clear on class.
And #5664 seems to happen because of this behavior.

function and type unbounded classmethod's should be checked by different algorithm.
I think it is needed to be fix checkexpr.py.

But this fix may be too large. So I'm wondering "Does it exist Plan B?"

@kitsuyui kitsuyui force-pushed the generic-classmethod-type branch from 4540a12 to e00f4c5 Compare October 13, 2018 14:52
@kitsuyui
Copy link
Author

I've finally found the answer myself.
I added error message of when accessing classmethod of generic class contains type variables.

from typing import Generic, TypeVar
T = TypeVar('T')
S = TypeVar('S')
class A(Generic[T]):
    @classmethod
    def f(cls) -> T: pass
class B(A[S]): pass
A.f  # E: Access to classmethod of generic type remains type variables T is ambiguous.
B.f  # E: Access to classmethod of generic type remains type variables S is ambiguous.

@ilevkivskyi

Would you please review this once more?

@ilevkivskyi
Copy link
Member

Sorry for another delay. I will try to review this in the next few days.

@kitsuyui
Copy link
Author

@ilevkivskyi

Thank you.
And I've just found myself that my branch has false negative like following case;

from typing import Generic, TypeVar


T = TypeVar('T')


class X(Generic[T]):
    @classmethod
    def a(cls) -> T:
        ...

    @classmethod
    def b(cls) -> T:
        return cls.a()

This should be treated as valid but error: Access to classmethod of generic type remains type variables T is ambiguous. will be happen.

So I'm planning to add more refactoring and more test cases. Please review after this.

@kitsuyui kitsuyui force-pushed the generic-classmethod-type branch from e00f4c5 to fbd2a71 Compare October 22, 2018 15:16
@kitsuyui
Copy link
Author

@ilevkivskyi
I did it. Please review this.

@kitsuyui kitsuyui force-pushed the generic-classmethod-type branch from fbd2a71 to 9cf96d5 Compare October 22, 2018 15:27
@kitsuyui kitsuyui force-pushed the generic-classmethod-type branch 2 times, most recently from d0aa3c7 to 9b3a114 Compare October 23, 2018 06:06
Copy link
Member

@ilevkivskyi ilevkivskyi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is another pass of review. Unfortunately, this is still not ready.

if base_instance.type.bases:
typ = expand_type_by_instance_bases(typ, base_instance)
typ = expand_type_by_instance(typ, base_instance)
return typ
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The algorithm is still wrong. For example it will not work for something like this:

class B(Generic[T]):
    @classmethod
    def f(cls) -> T: ...
class C(B[Tuple[T, T]]):
    ...
class D(C[str]):
    ...
reveal_type(D.f())  # Revealed type is 'str', but should be 'Tuple[str, str]'

This is not just a small bug there, it is "conceptually" wrong. As I already said twice, you should use something similar to map_instance_to_supertype from maptype.py. If this is too complex, then I can overtake this PR, it already takes too long.

@kitsuyui
Copy link
Author

I'm going to fix them in a few days.

@kitsuyui kitsuyui force-pushed the generic-classmethod-type branch 2 times, most recently from 84690f7 to 00b1b86 Compare October 30, 2018 17:47
@kitsuyui
Copy link
Author

@ilevkivskyi

I did it. Please review this again?

  • Added test-case that class method return type depends on type variables from two different bases.
  • Fixed test-cases and error messages calling directly generic class method such like A[str].f().
  • Added newlines between logical blocks for readability.

@ilevkivskyi
Copy link
Member

I did it. Please review this again?

You didn't fix the main problem (although you marked it as resolved, why?), as I mentioned in two previous reviews, you algorithm is "conceptually" wrong.

It looks like it doesn't make sense to push this forward any more.

@kitsuyui
Copy link
Author

kitsuyui commented Oct 31, 2018

@ilevkivskyi
I had closed it by misunderstanding twice.
It is better that you solve this than I do. Please over take this if you can.
I'm so sorry.

@kitsuyui kitsuyui force-pushed the generic-classmethod-type branch 4 times, most recently from cc0084c to 536225f Compare November 2, 2018 18:17
Analyze generic types in classmethod by map_instance_to_supertype and expand_type_by_instance.
Add "get_classmethod" method to TypeInfo because get_method cannot treat classmethod.
@kitsuyui kitsuyui force-pushed the generic-classmethod-type branch from 536225f to bbfd94c Compare November 2, 2018 18:32
@kitsuyui
Copy link
Author

kitsuyui commented Nov 2, 2018

@ilevkivskyi Sorry for taking your time again.

I rewrote the code to use map_instance_to_supertype.
I read the code that using map_instance_to_supertype for ordinary method.

The code for analyze ordinary method uses TypeInfo.get_method(name). This method doesn't suit for classmethod.
So I add get_classmethod to TypeInfo.

And I added a test for #5420 (comment)

How does this?

@kitsuyui kitsuyui closed this Nov 19, 2018
@ilevkivskyi
Copy link
Member

@kitsuyui Thank you for your work! It is a useful starting point for me to try making a better fix for this. Don't worry that this PR didn't make it. You just took a bit too hard issue as a first time contribution.

@kitsuyui
Copy link
Author

Thank you @ilevkivskyi !
I'm very thankful for mypy. I will take more easier issue next.

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

Successfully merging this pull request may close these issues.

4 participants