pylint
pylint copied to clipboard
Pylint does not appear to understand the descriptor protocol
Bug description
Writing a custom @property-like descriptor causes pylint to issue various false postives. Example code:
"Demonstrate class property problems"
from typing import Callable
class ClassROProperty(property):
"Decorator class to create a read-only class property"
def __init__(self, getter:Callable):
self._getter = getter
def __get__(self, _, cls):
return self._getter(cls)
class MyClass:
"A class which has only class properties"
@ClassROProperty
def prop_1(cls):
"Return an iterable value"
return [1, 2, 3, 4]
for val in MyClass.prop_1:
print(val)
print(MyClass.prop_1[1])
Running pylint --disable=too-few-public-methods on this results in these failures:
************* Module test
test.py:17:4: E0213: Method 'prop_1' should have "self" as first argument (no-self-argument)
test.py:21:11: E1133: Non-iterable value MyClass.prop_1 is used in an iterating context (not-an-iterable)
test.py:23:6: E1136: Value 'MyClass.prop_1' is unsubscriptable (unsubscriptable-object)
------------------------------------------------------------------
Your code has been rated at 0.00/10 (previous run: 0.00/10, +0.00)
E0213 can be fairly easily handled by disabling no-self-argument module- or class-wide. But E1133 and E1136 are much more difficult to suppress, because they happen everywhere the properties are used, not where they are defined. So anyone using your code needs to know to suppress these errors in their code.
I raised this on discord and was asked to file a report here.
Command used
venv/bin/pylint --disable=too-few-public-methods test.py
Pylint output
************* Module test
test.py:17:4: E0213: Method 'prop_1' should have "self" as first argument (no-self-argument)
test.py:21:11: E1133: Non-iterable value MyClass.prop_1 is used in an iterating context (not-an-iterable)
test.py:23:6: E1136: Value 'MyClass.prop_1' is unsubscriptable (unsubscriptable-object)
------------------------------------------------------------------
Your code has been rated at 0.00/10 (previous run: 0.00/10, +0.00)
Expected behavior
These appear to be false positives.
Pylint version
pylint 2.15.6
astroid 2.12.13
Python 3.10.6 (main, Nov 2 2022, 18:53:38) [GCC 11.3.0]
OS / Environment
Ubuntu 22.04
Bumping.
Pylint still doesn't understand descriptors at all (except for the few ones that seem to be hardcoded in pylint, like classmethod, property etc).
Edit: here's an example that confuses pylint:
from typing import Any, Callable, Concatenate, Generic, ParamSpec, TypeVar, overload
from typing_extensions import Self
IR = TypeVar("IR")
IP = ParamSpec("IP")
KR = TypeVar("KR")
KP = ParamSpec("KP")
IT = TypeVar("IT")
KT = TypeVar("KT")
class Hybrid(Generic[IT, KT, IP, IR, KP, KR]):
def __init__(
self,
instance_method: Callable[Concatenate[IT, IP], IR],
klass_method: Callable[Concatenate[type[KT], KP], KR],
):
self.im = instance_method
self.km = klass_method
def __call__(self, _: Any) -> Self:
return self
@overload
def __get__(self, obj: IT, objtype: Any) -> Callable[IP, IR]: ...
@overload
def __get__(self, obj: None, objtype: type[KT]) -> Callable[KP, KR]: ...
def __get__(self, obj: Any, objtype: Any = None):
if obj is None:
return self.km.__get__(obj, objtype)
return self.im.__get__(obj, objtype)
class IntAdder:
def __init__(self, v: int):
self.v = v
def _i_add(self, b: int) -> int:
return self.v + b
@classmethod
def _c_add(cls, a: int, b: int) -> int:
return a + b
@Hybrid(_i_add, _c_add)
def add(self): # pylint: disable=missing-function-docstring
...
assert IntAdder(3).add(4) == 7
assert IntAdder.add(3, 4) == 7
A from-the-hip naive patch that solves #9832 but causes some failures worth understanding:
diff --git a/astroid/bases.py b/astroid/bases.py
index 4a684cf1..3dfd71f1 100644
--- a/astroid/bases.py
+++ b/astroid/bases.py
@@ -44,9 +44,7 @@ if PY310_PLUS:
# List of possible property names. We use this list in order
# to see if a method is a property or not. This should be
-# pretty reliable and fast, the alternative being to check each
-# decorator to see if its a real property-like descriptor, which
-# can be too complicated.
+# pretty reliable and fast.
# Also, these aren't qualified, because each project can
# define them, we shouldn't expect to know every possible
# property-like decorator!
@@ -87,6 +85,8 @@ def _is_property(
if inferred is None or isinstance(inferred, UninferableBase):
continue
if isinstance(inferred, nodes.ClassDef):
+ if inferred.lookup("__get__"):
+ return True
for base_class in inferred.bases:
if not isinstance(base_class, nodes.Name):
continue