-
Notifications
You must be signed in to change notification settings - Fork 1.5k
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
Descriptor changing its class return type when object sets something to it #2698
Comments
The problem here is that you haven't declared the type of attribute class Model:
foo2: Descriptor = Descriptor()
... I'll think more about whether the inference logic could handle this better. One challenge is that execution order matters here, and in general a static type checker can't predict execution order. In your particular case, it would be able to assume a partial execution order because class Model:
def method1(self):
self.foo = Descriptor()
def method2(self):
self.foo = "hi"
m1 = Model()
m1.method1()
m1.method2()
print(m1.foo)
m2 = Model()
m2.method2()
m2.method1()
print(m2.foo) In this case, the value of Another challenge is in cases where a descriptor is conditionally assigned in a class. class Model:
if condition:
foo2 = Descriptor()
else:
foo2 = "abc"
def some_method_that_sets_foo2(self):
self.foo2 = "abc" Perhaps the right answer is to say that you must declare the type of the attribute in this case. That eliminates these ambiguities and makes the intent clear. |
Hey @erictraut , In the first example I don't understand how it is similar to the issue. I mean, if you are setting class Model:
foo = Descriptor()
def method1(self):
self.foo = Descriptor() Then you would be actually calling In the second example I can see how it is ambiguous, but I also don't understand how that is related to the issue. And just to mention, that is not too different from using a property: If Summarizing, if the class defines an attribute as a descriptor, it totally changes what |
Python is pretty loose when it comes to differentiating between class and instance variables. A class variable can be accessed in the same manner as an instance variable through a
There's a critical difference between your first example and a property. A |
Thinking about this more, I think this is a case where you will need to provide a type declaration rather than relying on type inference. The inference rules are already very complex and expensive. I don't think it makes sense to add more logic to handle this special case and all of the edge cases I mentioned above. |
Hey @erictraut , I understand what you mean regarding class and instance variables, due to the fact that But that doesn't apply to descriptors. Descriptors work in a way that, even if For example: class Descriptor:
def __init__(self):
self._values = {}
def __get__(self, obj, objtype):
if obj is None:
return self
return "descriptor get: " + self._values.get(obj, "<not set>")
def __set__(self, obj, value):
self._values[obj] = value
class Foo:
desc = Descriptor()
f = Foo()
print(f.desc)
print(Foo.desc)
f.desc = "abc"
print(f.desc)
print(Foo.desc)
f.__dict__["desc"] = "def"
print(f.desc)
print(Foo.desc) Executing that I get:
When I set
I don't understand the internal type checking implicances here but, even though I didn't declare the type, since the assignment is a descriptor it will not change unless set directly at the class. The comparison to
I would like to kindly ask you to reconsider this. Descriptors are pretty core python stuff (e.g. without them we could not have Also, you mentioned mypy, but it supports the descriptor protocol: python/mypy#2266 IMO, there are basically 2 issues here:
|
Pyright already has support for descriptors. If you add a type annotation for class Model:
foo2: Descriptor = Descriptor() |
@erictraut it does solve some problems but not all. Let me give you an example based on the original code: from typing import TypeVar, overload
_S = TypeVar("_S", bound="Descriptor")
_M = TypeVar("_M", bound="Model")
class Descriptor:
@overload
def __get__(self: _S, obj: None, objtype: type[_M]) -> _S:
...
@overload
def __get__(self, obj: _M, objtype: type[_M]) -> str:
...
def __set__(self, obj, value: str):
...
class Model:
foo = Descriptor()
foo2: Descriptor = Descriptor()
def some_method_that_sets_foo2(self):
reveal_type(self.foo)
reveal_type(type(self).foo)
self.foo = "abc"
reveal_type(self.foo)
reveal_type(type(self).foo)
reveal_type(self.foo2)
reveal_type(type(self).foo2)
self.foo2 = "abc"
reveal_type(self.foo2)
reveal_type(type(self).foo2) The output of this is:
I can see that, as you said, Having said that, there's an error in that even for |
The def func1(val: int):
x: int = val
reveal_type(x) # int
x = 3
reveal_type(x) # Literal[3] or class A:
a: int
def func2(val: A):
reveal_type(val.a) # int
val.a = 3
reveal_type(val.a) # Literal[3] The same thing is happening in your example above. So, this is all behaving as expected. I recently (yesterday) added logic to avoid assignment-based type narrowing for asymmetric descriptors (where the getter return type doesn't match the setter type). This will be in the next release. See this issue for more details. But this doesn't apply to your descriptor because it's not asymmetric. |
Hey @erictraut ,
I understand why the type narrowing on assignment makes sense from the point of a variable, but I think it is too much to consider the same for a descriptor as it is only considering the ones that acts like a "property". I mean, even though the types match doesn't mean that what I set is what I get. Let me demonstrate a real working example: from typing import Any, TypeVar, overload
_S = TypeVar("_S", bound="PowerOfTwo")
class PowerOfTwo:
def __init__(self):
self._values = {}
@overload
def __get__(self: _S, obj: None, objtype: Any) -> _S:
...
@overload
def __get__(self, obj: Any, objtype: Any) -> int:
...
def __get__(self, obj, objtype):
if obj is None:
return self
return self._values.get(obj, 0) ** 2
def __set__(self, obj, value: int):
self._values[obj] = value
class Foo:
var: PowerOfTwo = PowerOfTwo()
f = Foo()
reveal_type(f.var) # Type of "f.var" is "int"
f.var = 4
reveal_type(f.var) # Type of "f.var" is "Literal[4]"
print(f.var) # prints 16 This is basically what I'm saying ^. Setting anything to a descriptor doesn't imply that it will return that, even if it is symetric. In that example, it says that f.var is edit: changed the output of the code as comments in the example itself for clarity |
Yes, this is a case where practicality trumps type correctness. There has been significant discussion of this already. See this issue for a discussion and resolution. We're unlikely to revisit this. |
@erictraut I don't see how that discussion in the issue you pointed is "resolved" like that, for me it seems like an open discussion still. Also, a comment from gvanrossum just after your summary points to the exact same issue I'm pointing here, that narrowing the type of descriptors is wrong. His example is asymmetric, mine just happens to be symmetric, but the idea is the same.
I can see why and respect that for symmetrical And speaking of the original issue itself, you still didn't give me any reply on it yet. Taking a non-descriptor as an example: class Foo:
a = 1
f = Foo()
reveal_type(f.a) # Type of "f.a" is "int"
reveal_type(Foo.a) # Type of "Foo.a" is "int"
f.a = "abc"
reveal_type(f.a) # Type of "f.a" is "Literal['abc']"
reveal_type(Foo.a) # Type of "Foo.a" is "int" This is what I was expecting ^. Even though I changed You said that it was because the type was being inferred, but this example is too being inferred but isn't suffering from the same issue (or else the last |
We've meandered all over the place in this issue. Let me try to summarize.
I'm going to consider this issue closed at this point. If you see other issues, please feel open another bug report or feature request. |
So, to summarize, descriptors, an essential and widely used part of the Python language, are not supported by Pyright and never will be? That is a bit disappointing, I understand it's a lot of work, but maybe it should at least be on your roadmap? |
Considering this self-contained example:
Running pyright gives the following
The fact that there exists a method that sets
foo2
makes the typing for the descriptor being retrieved from the class to consider that now he can return that too, which is wrong since I'm setting it through object and not class. Also, setting it by class (i.e.Model.foo = <something else>
would change it tosomething else
and not call__set__
).The text was updated successfully, but these errors were encountered: