-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
classmethod produces errors due to TypeVar #9456
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
Comments
@gvanrossum I'm not sure I follow the reasoning on the |
I just had a thought. IIRC for a "type var with value restriction" (like you have here) mypy actually tries to type-check the entire class/function twice (or more if there are more than 2 values). This seems like two different instances of a problem with that (I think the error on Alas, I can't help you much more than that, it will require some serious trawling through the source code to figure this out. Thanks for the report though! |
I dug into the code and found that it comes from here: Lines 1400 to 1409 in eb50379
It checks for each possible combination, so 4 type checks with these substitutions:
I'm not familiar with TypeVars so I'm not sure how to fix this. Would it make sense to check on an UnionType of the 2 constraints? I suppose it wasn't written like that for a reason though. |
I think the behaviour looks wrong for the classmethod, where we're passing the variable in, so it should be valid if it's of any subtype. For the init method, it sounds reasonable to test it with each combination of inputs for any errors. However, it should only raise an unreachable error if that error occurs in every case. |
…striction This paragraph explains the limitation with TypeVars: https://github.com/python/mypy/blob/eb50379defc13cea9a8cbbdc0254a578ef6c415e/mypy/checker.py#L967-#L974 We currently have no way of checking for all the type expansions, and it's causing the issue python#9456 Using `self.chk.should_report_unreachable_issues()` honors the suppression of the unreachable warning for TypeVar with value restrictions
…striction (#9572) This paragraph explains the limitation with TypeVars: https://github.com/python/mypy/blob/eb50379defc13cea9a8cbbdc0254a578ef6c415e/mypy/checker.py#L967-#L974 We currently have no way of checking for all the type expansions, and it's causing the issue #9456 Using `self.chk.should_report_unreachable_issues()` honors the suppression of the unreachable warning for TypeVar with value restrictions
I've just updated the original description to focus on the remaining classmethod issue. |
i think mypy is correct here. at first glance it looks like it's bounding the generic when it shouldn't, but it's possible for the generic to already have a concrete type by the time the classmethod is called. for example: from __future__ import annotations
from typing import Generic, TypeVar
T = TypeVar("T", int, str)
class A(Generic[T]):
@classmethod
def func(cls, arg: int) -> A[T]:
return cls(arg)
def __init__(self, arg: T):
self.attr: T = arg
asdf: A[str] = A.func(1)
reveal_type(asdf) # Revealed type is "__main__.A[builtins.str]" |
Not sure I understand your argument, where is the concrete type for the class? In your example, you call |
i'm saying that the generic could have a concrete type here, and that mypy is right to complain, though that example i gave probably isn't very clear. what about this one: from typing import Generic, TypeVar
T = TypeVar("T", int, str)
class A(Generic[T]):
class_attr: T
@classmethod
def set_attr(cls, arg: int) -> None:
# mypy is right to complain here
cls.class_attr = arg # error: Incompatible types in assignment (expression has type "int", variable has type "str") [assignment]
B = A[str]
B.set_attr(1)
value = B().class_attr
# if it didn't complain, this would happen:
reveal_type(value) # note: Revealed type is "builtins.str*"
print(value) # 1 (though i think the error message itself is bad, see #11536) anyway, looks like the solution is to just type the class A(Generic[T]):
class_attr: T
@classmethod
def set_attr(cls: type[A[int]], arg: int) -> None:
# error is gone
cls.class_attr = arg
B = A[str]
#now we correcltly get an error here instead
B.set_attr(1) so for your original example: class A(Generic[T]):
@classmethod
def func(cls: type[A[int]], arg: int):
# no more error
return cls(arg)
def __init__(self, arg: T):
self.attr = arg |
I'm afraid there is no such thing as a concrete class type as you've just described, the example you managed to pass in mypy should in fact have given errors as it is completely broken. Try expanding your example to:
There is only 1 concrete class, Therefore, even if you have done |
I'm not suggesting removing any valid checks, I'm suggesting that classmethods should be checked with the first argument being a generic type, because that's exactly what it is, instead of checking it for each concrete type, which can never actually happen. e.g. This should also work without problems:
The fact that |
And if I add your 'fix' of specifying the cls type as |
@Dreamsorcerer Don't forget about polymorphic This error is entirely correct, just perhaps in your example it's not being abused. from __future__ import annotations
from typing import Generic, TypeVar
T = TypeVar("T", int, str)
class A(Generic[T]):
@classmethod
def func(cls, arg: int) -> A[int]:
return cls(arg) # error: Incompatible return value type (got "A[str]", expected "A[int]")
def foo(self, arg: T) -> None:
pass
def __init__(self, arg: T):
self.attr: T = arg
class B(A[str]):
def foo(self, arg: str) -> None:
print(arg.upper())
b1: A[str] = B("foo")
b2: A[int] = b1.func(1)
b2.foo(1) # runtime error: AttributeError: 'int' object has no attribute 'upper' for example def foo(a: int) -> str:
return str(a)
foo("AMONGUS") # error: Argument 1 to "foo" has incompatible type "str"; expected "int" Why would this complain if it's not going to fail at runtime? This issue is that even though your specific example will work correctly at runtime, it's not typesafe and will most certainly lead to type errors down the line. |
Generally a concrete type refers to any type that has no unspecified type parameters. By that definition, from __future__ import annotations
from typing import Generic, TypeVar, ClassVar
T = TypeVar("T")
class A(Generic[T]):
@classmethod
def foo(cls, a: T) -> None:
...
@classmethod
def bar(cls: type[A[int]]) -> None:
...
Int = A[int] # concrete type
Str = A[str] # concrete type
A_Alias = A # non-concrete type
A_Alias.foo(1) # correct usage not as an annotation
Str.foo("AMONGUS") # correct usage not as an annotation
Int.bar() # correct usage not as an annotation |
Yep, that certainly adds some complication when inheriting from a defined type. I don't know if maybe mypy can detect that such a classmethod cannot be used in a subclass or something, but sounds a bit messy either way.
I don't see the usecase for your last example. Why does it matter what type you assigned as the class for that classmethod? If you stick with the idea that
What practical purpose is there to have |
That would break LSP, mypy is correct to point out this invalid code.
Thats how the type system works, it doesn't matter if you can't think of a use case for it.
You can't say that |
If you made the class from typing import Generic, TypeVar, final
T = TypeVar("T")
@final
class A(Generic[T]):
@classmethod
def func(cls, arg: int) -> A[int]:
return cls(arg) # error: Argument 1 to "A" has incompatible type "int"; expected "T"
def __init__(self, arg: T):
self.attr: T = arg This just looks very inconsistent though, and special casing this behavior would just increase complexity unnecessarily and cause more confusion on the users end in my opinion. And at that point, why not just do: @classmethod
def func(cls, arg: int) -> A[int]:
return A(arg) # no error I think this issue should be closed as I don't see anything wrong here. |
I agree that mypy is doing the right thing in this case. The type of the |
A minimal test case:
This gives an error complaining that
cls(arg)
expectsNone
, due to it expanding the TypeVar on the class object.Expected behaviour is that no error is produced, and that the type of
cls
does not have the expandedTypeVar
(i.e. it should receive the same type that would be seen on a separate line of code like:a = A(5)
.One catch to changing this behaviour is correct handling when subclassing with a defined type: #9456 (comment)
I've had a go at this, but was unable to figure out how to fix it. This is what I've figured out though:
https://github.com/python/mypy/blob/master/mypy/semanal.py#L641
Within
prepare_method_signature()
,cls
gets changed fromAny
to the class type.This is done by calling
fill_typevars()
, which results in the type includingT
, rather than just remaining as aTypeInfo
(which appears to be the type in an example such asa = A(5)
).I've tried just removing the
fill_typevars()
, but then it runs into an unimplementedRuntimeError
.Because it includes
T
at this point, when it later tries to typecheck the code, it will check the class method twice, once asA[int]
and again asA[None]
, which obviously throws the error we see.So, it seems like there needs to be some way to stop class methods getting the
TypeVar
s filled out on thecls
argument.The text was updated successfully, but these errors were encountered: