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

@classmethod of Generic class #1337

Closed
tharvik opened this issue Apr 6, 2016 · 12 comments · Fixed by #6418
Closed

@classmethod of Generic class #1337

tharvik opened this issue Apr 6, 2016 · 12 comments · Fixed by #6418
Labels
bug mypy got something wrong priority-1-normal

Comments

@tharvik
Copy link
Contributor

tharvik commented Apr 6, 2016

from typing import TypeVar, Generic

T = TypeVar('T')


class A(Generic[T]):
    @classmethod
    def func(cls) -> T:
        pass


class B(A['B']):
    pass


def except_b(b: B):
    pass


except_b(B.func())

is a valid python3 code, but mypy warns that

file:20: error: Argument 1 to "except_b" has incompatible type "T"; expected "B"

but the resolved pseudo-code of class B should be

class B(A['B']):
    @classmethod
    def func(cls) -> 'B':
        pass

if I replace it, mypy doesn't complain anymore.

I'm might have overlook some specificities of @classmethod but I think that func is also bind to class B thus should be type checking.

@gvanrossum
Copy link
Member

@rwbarton Another typevar mystery you might look into?

@JukkaL JukkaL added the bug mypy got something wrong label Apr 7, 2016
@gvanrossum gvanrossum added this to the 0.3.2 milestone Apr 7, 2016
@JukkaL
Copy link
Collaborator

JukkaL commented Apr 14, 2016

Actually it seems that the signature of A.func should be rejected, since T is not defined at class/static method level, since the type variable is specific to an instance, and we have no self. Type variables should not be in scope for static or class methods. I'm going to move this to 0.4 since this seems like a pretty rare edge case.

@JukkaL JukkaL modified the milestones: 0.4.0, 0.3.2 Apr 14, 2016
@rwbarton
Copy link
Contributor

It also seems consistent to turn the class typevars into function typevars for a static method call, like we do for constructors. We could give A.func the type def [T] () -> T, and B.func the type def () -> B. Hard to say whether that is useful without some realistic examples.

@gvanrossum
Copy link
Member

gvanrossum commented Apr 14, 2016 via email

@rwbarton
Copy link
Contributor

This is also an issue for class attributes:

class A(Generic[T]):
    a = []  # type: List[T]
class B(A[int]): pass

What is the type of B.a? For that matter what is the type of A.a?

This issue is actually more severe, because on the one hand we cannot give A.a any sort of generic type (only functions can be generic in mypy currently, and besides it would not be type safe to give A.a a type like forall T. List[T], as it is a mutable variable); and on the other hand the style of declaring class variables in order to specify the types of instance variables is used frequently, at least in mypy itself, and if we want to do the same in generic classes, we need to be able to make declarations like that of A.

@gvanrossum
Copy link
Member

Yeah, but mypy's handling of class attributes is just really poor, and the
meaning of that example isn't clear, because we do type erasure -- at
runtime there's only one A and it has only one a.

OTOH for classmethod/staticmethod it makes sense to be generic? Well I
guess interpreting them as generic functions works too, so actually Jukka
is right about that. Sorry.

@tharvik
Copy link
Contributor Author

tharvik commented Apr 15, 2016

Hard to say whether that is useful without some realistic examples.

The issue was encounter when I wanted to force a subclass to implement a method to return an instance of the subclass (and this way, force a constructor).

If you want to have some code example (the main idea is to parse XML from nexpose API), it is the XmlParse class which force his childs to have the from_xml method. You can see how it used in the Message class.

@JukkaL
Copy link
Collaborator

JukkaL commented Apr 15, 2016

@tharvik I'm not sure if your example is possible to be fully annotated via PEP 484, as type variables are specific to instances. Currently having an Any return type in the XmlParse class seems like the only option. If/when mypy will support 'self types' (#1212), they might be helpful in your use case.

@rwbarton If a class-level variable has a type based on a type variable, mypy should enforce that this variable should only be accessed via an instance, not via the class object. If we add that restriction, things should be fine.

Having generic class methods and static methods is fine, but they should perhaps use a separate type variable, not a class-level type variable. However, we could implicitly understand a class-level type variable as a method-level one if used in a static or class method, but that might be a little confusing.

@methane
Copy link
Member

methane commented Jun 2, 2016

Here is another realistic example.
https://gist.github.com/methane/d18c66d38b1916e7c36883c0a83f0a01

This demonstrates "Repository Pattern". [1]
Since repository is singleton for many cases, I want to implement it by classmethod.

[1] https://msdn.microsoft.com/en-us/library/ff649690.aspx

@rwbarton
Copy link
Contributor

@JukkaL, is there anything wrong with continuing to turn class type variables that appear in class methods into function type variables, like we do now (and very similar to how the signature of __init__ is treated)?

What I'm going to implement is just mapping class type variables from the superclass before turning them into function type variables (as already happens for __init__). I think this will allow the above use case.

@JukkaL
Copy link
Collaborator

JukkaL commented Jun 29, 2016

@rwbarton I think it's fine to turn class type variables into function type variables, but if we make this a real feature we should test that there is unlikely to be confusion (by mypy) between two different kinds of type variables in a single scope, as discussed above.

@elazarg
Copy link
Contributor

elazarg commented Oct 19, 2017

This should be possible to solve with a slightly better unification for self-type (currently it does only the minimal effort).

class A(Generic[T]):
    @classmethod
    def func(cls: Type[A[Q]]) -> Q: ...

ilevkivskyi added a commit that referenced this issue Feb 24, 2019
Fixes #3645
Fixes #1337
Fixes #5664

The fix is straightforward, I just add/propagate the bound type variable values by mapping to supertype.

I didn't find any corner cases with class methods, and essentially follow the same logic as when we generate the callable from `__init__` for generic classes in calls like `C()` or `C[int]()`.

For class attributes there are two things I fixed. First we used to prohibit ambiguous access:
```python
class C(Generic[T]):
    x: T
C.x  # Error!
C[int].x  # Error!
```
but the type variables were leaking after an error, now they are erased to `Any`. Second, I now make an exception and allow accessing attributes on `Type[C]`, this is very similar to how we allow instantiation of `Type[C]` even if it is abstract (because we expect concrete subclasses there), plus this allows accessing variables on `cls` (first argument in class methods), for example:
```python
class C(Generic[T]):
    x: T
    def get(cls) -> T:
        return cls.x  # OK
```

(I also added a bunch of more detailed comments in this part of code.)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug mypy got something wrong priority-1-normal
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants